panelui-native 0.76.0 → 0.78.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.
Files changed (125) hide show
  1. package/lib/module/components/ai-input/index.js +23 -7
  2. package/lib/module/components/ai-input/index.js.map +1 -1
  3. package/lib/module/components/area-chart/index.js +32 -25
  4. package/lib/module/components/area-chart/index.js.map +1 -1
  5. package/lib/module/components/bottom-sheet/index.js +98 -15
  6. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  7. package/lib/module/components/button/index.js +7 -1
  8. package/lib/module/components/button/index.js.map +1 -1
  9. package/lib/module/components/funnel-chart/index.js +15 -2
  10. package/lib/module/components/funnel-chart/index.js.map +1 -1
  11. package/lib/module/components/heatmap-chart/index.js +19 -27
  12. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  13. package/lib/module/components/hex-chart/index.js +2 -0
  14. package/lib/module/components/hex-chart/index.js.map +1 -1
  15. package/lib/module/components/line-chart/index.js +38 -31
  16. package/lib/module/components/line-chart/index.js.map +1 -1
  17. package/lib/module/components/marquee/index.js +91 -5
  18. package/lib/module/components/marquee/index.js.map +1 -1
  19. package/lib/module/components/pie-chart/index.js +15 -2
  20. package/lib/module/components/pie-chart/index.js.map +1 -1
  21. package/lib/module/components/plot/index.js +42 -44
  22. package/lib/module/components/plot/index.js.map +1 -1
  23. package/lib/module/components/progress-button/index.js +390 -0
  24. package/lib/module/components/progress-button/index.js.map +1 -0
  25. package/lib/module/components/progress-button/progress-button-hold.js +57 -0
  26. package/lib/module/components/progress-button/progress-button-hold.js.map +1 -0
  27. package/lib/module/components/scatter-chart/index.js +6 -25
  28. package/lib/module/components/scatter-chart/index.js.map +1 -1
  29. package/lib/module/components/section-rail/index.js +40 -17
  30. package/lib/module/components/section-rail/index.js.map +1 -1
  31. package/lib/module/components/select/index.js +1 -0
  32. package/lib/module/components/select/index.js.map +1 -1
  33. package/lib/module/components/slider/index.js +1 -0
  34. package/lib/module/components/slider/index.js.map +1 -1
  35. package/lib/module/components/split-view/index.js +54 -10
  36. package/lib/module/components/split-view/index.js.map +1 -1
  37. package/lib/module/components/switch/index.js +1 -0
  38. package/lib/module/components/switch/index.js.map +1 -1
  39. package/lib/module/components/timeline/index.js +145 -14
  40. package/lib/module/components/timeline/index.js.map +1 -1
  41. package/lib/module/components/toast/index.js +45 -11
  42. package/lib/module/components/toast/index.js.map +1 -1
  43. package/lib/module/components/treemap-chart/index.js +63 -6
  44. package/lib/module/components/treemap-chart/index.js.map +1 -1
  45. package/lib/module/components/waterfall-chart/index.js +20 -4
  46. package/lib/module/components/waterfall-chart/index.js.map +1 -1
  47. package/lib/module/hooks/index.js +1 -0
  48. package/lib/module/hooks/index.js.map +1 -1
  49. package/lib/module/hooks/use-scroll-sections.js +44 -2
  50. package/lib/module/hooks/use-scroll-sections.js.map +1 -1
  51. package/lib/module/hooks/use-skeleton-handoff.js +58 -0
  52. package/lib/module/hooks/use-skeleton-handoff.js.map +1 -0
  53. package/lib/module/index.js +1 -0
  54. package/lib/module/index.js.map +1 -1
  55. package/lib/module/native/index.js.map +1 -1
  56. package/lib/typescript/src/components/ai-input/index.d.ts +24 -5
  57. package/lib/typescript/src/components/ai-input/index.d.ts.map +1 -1
  58. package/lib/typescript/src/components/area-chart/index.d.ts +1 -1
  59. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  60. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  61. package/lib/typescript/src/components/button/index.d.ts +15 -0
  62. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  63. package/lib/typescript/src/components/funnel-chart/index.d.ts.map +1 -1
  64. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  65. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
  66. package/lib/typescript/src/components/line-chart/index.d.ts +1 -1
  67. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  68. package/lib/typescript/src/components/marquee/index.d.ts +55 -2
  69. package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
  70. package/lib/typescript/src/components/pie-chart/index.d.ts.map +1 -1
  71. package/lib/typescript/src/components/plot/index.d.ts +1 -3
  72. package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
  73. package/lib/typescript/src/components/progress-button/index.d.ts +295 -0
  74. package/lib/typescript/src/components/progress-button/index.d.ts.map +1 -0
  75. package/lib/typescript/src/components/progress-button/progress-button-hold.d.ts +40 -0
  76. package/lib/typescript/src/components/progress-button/progress-button-hold.d.ts.map +1 -0
  77. package/lib/typescript/src/components/scatter-chart/index.d.ts +1 -1
  78. package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -1
  79. package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
  80. package/lib/typescript/src/components/split-view/index.d.ts +10 -1
  81. package/lib/typescript/src/components/split-view/index.d.ts.map +1 -1
  82. package/lib/typescript/src/components/timeline/index.d.ts +7 -0
  83. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  84. package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
  85. package/lib/typescript/src/components/treemap-chart/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -1
  87. package/lib/typescript/src/hooks/index.d.ts +1 -0
  88. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  89. package/lib/typescript/src/hooks/use-scroll-sections.d.ts +1 -0
  90. package/lib/typescript/src/hooks/use-scroll-sections.d.ts.map +1 -1
  91. package/lib/typescript/src/hooks/use-skeleton-handoff.d.ts +11 -0
  92. package/lib/typescript/src/hooks/use-skeleton-handoff.d.ts.map +1 -0
  93. package/lib/typescript/src/index.d.ts +3 -2
  94. package/lib/typescript/src/index.d.ts.map +1 -1
  95. package/lib/typescript/src/native/index.d.ts +17 -0
  96. package/lib/typescript/src/native/index.d.ts.map +1 -1
  97. package/package.json +1 -1
  98. package/src/components/ai-input/index.tsx +46 -12
  99. package/src/components/area-chart/index.tsx +49 -37
  100. package/src/components/bottom-sheet/index.tsx +110 -12
  101. package/src/components/button/index.tsx +20 -1
  102. package/src/components/funnel-chart/index.tsx +10 -2
  103. package/src/components/heatmap-chart/index.tsx +27 -22
  104. package/src/components/hex-chart/index.tsx +22 -6
  105. package/src/components/line-chart/index.tsx +57 -43
  106. package/src/components/marquee/index.tsx +129 -6
  107. package/src/components/pie-chart/index.tsx +10 -2
  108. package/src/components/plot/index.tsx +66 -50
  109. package/src/components/progress-button/index.tsx +465 -0
  110. package/src/components/progress-button/progress-button-hold.ts +57 -0
  111. package/src/components/scatter-chart/index.tsx +4 -25
  112. package/src/components/section-rail/index.tsx +39 -19
  113. package/src/components/select/index.tsx +1 -1
  114. package/src/components/slider/index.tsx +1 -1
  115. package/src/components/split-view/index.tsx +53 -8
  116. package/src/components/switch/index.tsx +1 -1
  117. package/src/components/timeline/index.tsx +143 -17
  118. package/src/components/toast/index.tsx +60 -11
  119. package/src/components/treemap-chart/index.tsx +65 -8
  120. package/src/components/waterfall-chart/index.tsx +14 -4
  121. package/src/hooks/index.ts +5 -0
  122. package/src/hooks/use-scroll-sections.ts +52 -3
  123. package/src/hooks/use-skeleton-handoff.ts +69 -0
  124. package/src/index.ts +10 -0
  125. package/src/native/index.ts +17 -0
@@ -66,18 +66,26 @@ import {
66
66
  isValidElement,
67
67
  useContext,
68
68
  useEffect,
69
+ useId,
69
70
  useImperativeHandle,
70
71
  useMemo,
71
72
  useRef,
72
73
  useState,
73
74
  type ReactNode,
74
75
  } from 'react';
75
- import { Pressable, View, type LayoutChangeEvent, type ViewProps } from 'react-native';
76
+ import {
77
+ Pressable,
78
+ View,
79
+ type LayoutChangeEvent,
80
+ type PressableProps,
81
+ type ViewProps,
82
+ } from 'react-native';
76
83
  import Animated, {
77
84
  Easing,
78
85
  cancelAnimation,
79
86
  useAnimatedProps,
80
87
  useDerivedValue,
88
+ useAnimatedStyle,
81
89
  useReducedMotion,
82
90
  useSharedValue,
83
91
  withRepeat,
@@ -89,7 +97,10 @@ import { useCSSVariable } from 'uniwind';
89
97
  import { Text } from '../../primitives/text';
90
98
  import { compactNumber, inkOn, useSeriesColor } from '../../utils/chart';
91
99
  import { cn } from '../../utils/cn';
100
+ import { useSkeletonHandoff } from '../../hooks/use-skeleton-handoff';
92
101
 
102
+ const AnimatedG = Animated.createAnimatedComponent(G);
103
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
93
104
  const AnimatedRect = Animated.createAnimatedComponent(Rect);
94
105
  const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
95
106
 
@@ -820,6 +831,9 @@ function TreemapChartSkeleton({ duration = 1400, color }: TreemapChartSkeletonPr
820
831
  const token = useCSSVariable('--color-skeleton');
821
832
  const base = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
822
833
  const highlight = useSeriesColor(undefined, 1);
834
+ // Unique per instance: a fixed id makes two of these on one screen share a
835
+ // gradient, and the second one to mount wins.
836
+ const gradientId = `panelui-treemap-skeleton-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
823
837
 
824
838
  const sweep = useSharedValue(0);
825
839
  const reducedMotion = useReducedMotion();
@@ -843,12 +857,16 @@ function TreemapChartSkeleton({ duration = 1400, color }: TreemapChartSkeletonPr
843
857
  x2: `${(sweep.value * 1.4 - 0.4 + 0.4) * 100}%`,
844
858
  }));
845
859
 
846
- if (!loading || width <= 0 || height <= 0) return null;
860
+ // Held through the fade rather than to the frame the data lands: the box has
861
+ // to dissolve while the tiles grow through it, or the panel is empty for as
862
+ // long as the first tile takes to become visible.
863
+ const { mounted, opacity } = useSkeletonHandoff(loading);
864
+ const fadeProps = useAnimatedProps(() => ({ opacity: opacity.value }));
847
865
 
848
- const gradientId = 'panelui-treemap-skeleton';
866
+ if (!mounted || width <= 0 || height <= 0) return null;
849
867
 
850
868
  return (
851
- <G>
869
+ <AnimatedG animatedProps={fadeProps}>
852
870
  <Defs>
853
871
  <AnimatedLinearGradient id={gradientId} animatedProps={animatedProps} y1="0" y2="0">
854
872
  <Stop offset="0" stopColor={base} />
@@ -864,7 +882,7 @@ function TreemapChartSkeleton({ duration = 1400, color }: TreemapChartSkeletonPr
864
882
  rx={cornerRadius}
865
883
  fill={`url(#${gradientId})`}
866
884
  />
867
- </G>
885
+ </AnimatedG>
868
886
  );
869
887
  }
870
888
  TreemapChartSkeleton.displayName = 'TreemapChart.Skeleton';
@@ -906,7 +924,7 @@ function TreemapChartLabels({
906
924
  formatValue,
907
925
  className,
908
926
  }: TreemapChartLabelsProps) {
909
- const { tiles, minLabelSize, status, activeIndex, setActiveIndex } =
927
+ const { tiles, minLabelSize, reveal, windows, status, activeIndex, setActiveIndex } =
910
928
  useChart('TreemapChart.Labels');
911
929
  /*
912
930
  * The tiles down the ramp are drawn part-transparent, so what a label sits on
@@ -929,8 +947,10 @@ function TreemapChartLabels({
929
947
  const percent = Math.round(tile.share * 100);
930
948
  const ink = inkOn(tile.color, behind, tile.strength);
931
949
  return (
932
- <Pressable
950
+ <TreemapChartLabel
933
951
  key={`${tile.label}-${index}`}
952
+ reveal={reveal}
953
+ window={windows[index] ?? FULL_WINDOW}
934
954
  accessibilityRole="button"
935
955
  accessibilityState={{ selected: activeIndex === index }}
936
956
  accessibilityLabel={`${tile.label}, ${format(tile.value, tile)}, ${percent} percent`}
@@ -962,12 +982,49 @@ function TreemapChartLabels({
962
982
  {percent}%
963
983
  </Text>
964
984
  ) : null}
965
- </Pressable>
985
+ </TreemapChartLabel>
966
986
  );
967
987
  })}
968
988
  </>
969
989
  );
970
990
  }
991
+
992
+ const FULL_WINDOW = { from: 0, to: 1 };
993
+
994
+ /**
995
+ * One tile's text, faded in on that tile's own slice of the reveal.
996
+ *
997
+ * Held at full opacity from the frame the data lands — which is what it used to
998
+ * do — every name and number sits at its final position over tiles that have no
999
+ * size yet, and the chart spends its whole entrance looking like a wireframe of
1000
+ * itself. The label belongs to the tile, so it arrives with it.
1001
+ */
1002
+ function TreemapChartLabel({
1003
+ reveal,
1004
+ window,
1005
+ children,
1006
+ style,
1007
+ ...props
1008
+ }: {
1009
+ reveal: SharedValue<number>;
1010
+ window: { from: number; to: number };
1011
+ } & Omit<PressableProps, 'children'> & { children: ReactNode; className?: string }) {
1012
+ const animatedStyle = useAnimatedStyle(() => {
1013
+ const span = window.to - window.from || 1;
1014
+ const raw = (reveal.value - window.from) / span;
1015
+ const clamped = raw < 0 ? 0 : raw > 1 ? 1 : raw;
1016
+ // Squared, so the text is still faint through the first half of the tile's
1017
+ // growth and only reads once there is a tile to read it on.
1018
+ return { opacity: clamped * clamped };
1019
+ });
1020
+
1021
+ return (
1022
+ <AnimatedPressable {...props} style={[style, animatedStyle]}>
1023
+ {children}
1024
+ </AnimatedPressable>
1025
+ );
1026
+ }
1027
+
971
1028
  TreemapChartLabels.displayName = 'TreemapChart.Labels';
972
1029
  TreemapChartLabels.slot = 'overlay' as const;
973
1030
 
@@ -43,6 +43,7 @@ import {
43
43
  isValidElement,
44
44
  useContext,
45
45
  useEffect,
46
+ useId,
46
47
  useImperativeHandle,
47
48
  useMemo,
48
49
  useRef,
@@ -73,7 +74,9 @@ import {
73
74
  } from '../../primitives/finite-chart';
74
75
  import { barPath, compactNumber, type Plot } from '../../utils/chart';
75
76
  import { cn } from '../../utils/cn';
77
+ import { useSkeletonHandoff } from '../../hooks/use-skeleton-handoff';
76
78
 
79
+ const AnimatedG = Animated.createAnimatedComponent(G);
77
80
  const AnimatedPath = Animated.createAnimatedComponent(Path);
78
81
  const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
79
82
 
@@ -983,6 +986,9 @@ function WaterfallChartSkeleton({
983
986
  const base = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
984
987
  const highlightToken = useCSSVariable('--color-chart-1');
985
988
  const highlight = typeof highlightToken === 'string' ? highlightToken : FALLBACK_TOTAL;
989
+ // Unique per instance: a fixed id makes two of these on one screen share a
990
+ // gradient, and the second one to mount wins.
991
+ const gradientId = `panelui-waterfall-skeleton-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
986
992
 
987
993
  const sweep = useSharedValue(0);
988
994
  const reducedMotion = useReducedMotion();
@@ -1032,12 +1038,16 @@ function WaterfallChartSkeleton({
1032
1038
  return path;
1033
1039
  }, [plot, horizontal, total, barGap, barWidth, cornerRadius]);
1034
1040
 
1035
- if (!loading || !d) return null;
1041
+ // Held through the fade rather than to the frame the data lands: the bars
1042
+ // grow out of the stubs, and cutting the stubs at the status change leaves
1043
+ // the panel empty until the first bar has length.
1044
+ const { mounted, opacity } = useSkeletonHandoff(loading);
1045
+ const fadeProps = useAnimatedProps(() => ({ opacity: opacity.value }));
1036
1046
 
1037
- const gradientId = 'panelui-waterfall-skeleton';
1047
+ if (!mounted || !d) return null;
1038
1048
 
1039
1049
  return (
1040
- <G>
1050
+ <AnimatedG animatedProps={fadeProps}>
1041
1051
  <Defs>
1042
1052
  <AnimatedLinearGradient id={gradientId} animatedProps={animatedProps} y1="0" y2="0">
1043
1053
  <Stop offset="0" stopColor={base} />
@@ -1046,7 +1056,7 @@ function WaterfallChartSkeleton({
1046
1056
  </AnimatedLinearGradient>
1047
1057
  </Defs>
1048
1058
  <Path d={d} fill={`url(#${gradientId})`} />
1049
- </G>
1059
+ </AnimatedG>
1050
1060
  );
1051
1061
  }
1052
1062
  WaterfallChartSkeleton.displayName = 'WaterfallChart.Skeleton';
@@ -30,6 +30,11 @@ export {
30
30
  type UseRevealProgressOptions,
31
31
  type UseRevealProgressResult,
32
32
  } from './use-reveal-progress';
33
+ export {
34
+ useSkeletonHandoff,
35
+ SKELETON_FADE,
36
+ type UseSkeletonHandoffResult,
37
+ } from './use-skeleton-handoff';
33
38
  export {
34
39
  useScrollSections,
35
40
  type UseScrollSectionsOptions,
@@ -24,7 +24,7 @@
24
24
  * <SectionRail value={sections.active} onValueChange={sections.scrollTo}>…</SectionRail>
25
25
  * ```
26
26
  */
27
- import { useCallback, useMemo, useRef, useState } from 'react';
27
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
28
28
  import type {
29
29
  LayoutChangeEvent,
30
30
  NativeScrollEvent,
@@ -54,6 +54,7 @@ export interface UseScrollSectionsResult {
54
54
  /** Spread onto the ScrollView. */
55
55
  scrollProps: {
56
56
  onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
57
+ onMomentumScrollEnd: () => void;
57
58
  scrollEventThrottle: number;
58
59
  };
59
60
  /** `onLayout` for a section's wrapper: `onLayout={measure(id)}`. */
@@ -62,6 +63,13 @@ export interface UseScrollSectionsResult {
62
63
  scrollTo: (id: string) => void;
63
64
  }
64
65
 
66
+ /**
67
+ * How long a programmatic scroll is given to arrive before the scroll handler
68
+ * starts believing positions again. Only reached when the scroll had nowhere to
69
+ * go and no momentum end ever fires.
70
+ */
71
+ const JUMP_TIMEOUT = 900;
72
+
65
73
  export function useScrollSections({
66
74
  ids,
67
75
  offset = 120,
@@ -70,6 +78,7 @@ export function useScrollSections({
70
78
  }: UseScrollSectionsOptions): UseScrollSectionsResult {
71
79
  const ref = useRef<ScrollView | null>(null);
72
80
  const offsets = useRef<Record<string, number>>({});
81
+ const settle = useRef<ReturnType<typeof setTimeout> | null>(null);
73
82
  const [active, setActive] = useState<string | undefined>(ids[0]);
74
83
 
75
84
  // Read inside the scroll handler, which must not be re-created on every
@@ -84,8 +93,23 @@ export function useScrollSections({
84
93
  []
85
94
  );
86
95
 
96
+ /*
97
+ * The section a `scrollTo` is travelling to, while it is still travelling.
98
+ *
99
+ * An animated scroll passes every section between here and there, and the
100
+ * scroll handler cannot tell those apart from sections the reader arrived at
101
+ * themselves — so it reported each one as active in turn. Downstream that is
102
+ * a real change of section every 16ms: a rail lighting up rows nobody chose,
103
+ * a haptic for each, and a jump that ends somewhere the reader watched it
104
+ * pass through. None of it happened; the reader asked for one section.
105
+ */
106
+ const jumpingTo = useRef<string | null>(null);
107
+
87
108
  const onScroll = useCallback(
88
109
  (event: NativeSyntheticEvent<NativeScrollEvent>) => {
110
+ // Mid-jump, the position is the animation's and not the reader's.
111
+ if (jumpingTo.current !== null) return;
112
+
89
113
  const { contentOffset, layoutMeasurement, contentSize } = event.nativeEvent;
90
114
  const list = idsRef.current;
91
115
  if (!list.length) return;
@@ -112,15 +136,40 @@ export function useScrollSections({
112
136
  (id: string) => {
113
137
  const top = offsets.current[id];
114
138
  if (top === undefined) return;
139
+ jumpingTo.current = id;
140
+ if (settle.current) clearTimeout(settle.current);
141
+ /*
142
+ * A backstop, not the normal way out. `onMomentumScrollEnd` does not fire
143
+ * for a scroll that had nowhere to go — a jump to the section already on
144
+ * screen — and without this the handler would stay muted for good.
145
+ */
146
+ settle.current = setTimeout(() => {
147
+ jumpingTo.current = null;
148
+ }, JUMP_TIMEOUT);
115
149
  setActive(id);
116
150
  ref.current?.scrollTo({ y: Math.max(top - scrollPadding, 0), animated: true });
117
151
  },
118
152
  [scrollPadding]
119
153
  );
120
154
 
155
+ const onMomentumScrollEnd = useCallback(() => {
156
+ if (settle.current) {
157
+ clearTimeout(settle.current);
158
+ settle.current = null;
159
+ }
160
+ jumpingTo.current = null;
161
+ }, []);
162
+
163
+ useEffect(
164
+ () => () => {
165
+ if (settle.current) clearTimeout(settle.current);
166
+ },
167
+ []
168
+ );
169
+
121
170
  const scrollProps = useMemo(
122
- () => ({ onScroll, scrollEventThrottle: 16 }),
123
- [onScroll]
171
+ () => ({ onScroll, onMomentumScrollEnd, scrollEventThrottle: 16 }),
172
+ [onScroll, onMomentumScrollEnd]
124
173
  );
125
174
 
126
175
  return { ref, active, scrollProps, measure, scrollTo };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Keeps a chart's placeholder on screen a beat past the data arriving, and
3
+ * dissolves it while the real marks grow in.
4
+ *
5
+ * A skeleton cut at the exact frame `status` flips leaves the plot empty: the
6
+ * placeholder is gone and the reveal has not started, so there is a blank frame
7
+ * between two states that were both supposed to show something. Outliving the
8
+ * status change by the length of the fade is what closes that gap, and it is
9
+ * why the loading state reads as *becoming* the chart rather than as being
10
+ * replaced by it.
11
+ *
12
+ * The fade runs on the UI thread; the unmount is scheduled back once, at the
13
+ * end, so nothing re-renders per frame.
14
+ *
15
+ * ```tsx
16
+ * const { mounted, opacity } = useSkeletonHandoff(status === 'loading');
17
+ * const animatedProps = useAnimatedProps(() => ({ opacity: opacity.value }));
18
+ * if (!mounted) return null;
19
+ * return <AnimatedG animatedProps={animatedProps}>{…}</AnimatedG>;
20
+ * ```
21
+ */
22
+ import { useEffect, useState } from 'react';
23
+ import {
24
+ runOnJS,
25
+ useReducedMotion,
26
+ useSharedValue,
27
+ withTiming,
28
+ type SharedValue,
29
+ } from 'react-native-reanimated';
30
+
31
+ /** Milliseconds for a placeholder to dissolve once the data arrives. */
32
+ export const SKELETON_FADE = 220;
33
+
34
+ export interface UseSkeletonHandoffResult {
35
+ /** True while the placeholder should render — through the fade, not just the wait. */
36
+ mounted: boolean;
37
+ /** The placeholder's opacity, `1` while loading and tweened to `0` after. */
38
+ opacity: SharedValue<number>;
39
+ }
40
+
41
+ export function useSkeletonHandoff(
42
+ loading: boolean,
43
+ duration: number = SKELETON_FADE
44
+ ): UseSkeletonHandoffResult {
45
+ const opacity = useSharedValue(loading ? 1 : 0);
46
+ // Mounted a beat longer than `loading`, so there is something to fade.
47
+ const [mounted, setMounted] = useState(loading);
48
+ const reducedMotion = useReducedMotion();
49
+
50
+ useEffect(() => {
51
+ if (loading) {
52
+ opacity.value = 1;
53
+ setMounted(true);
54
+ return;
55
+ }
56
+ // Reduced motion still wants the placeholder gone; it is the dissolve that
57
+ // is the motion, not the disappearance.
58
+ if (reducedMotion) {
59
+ opacity.value = 0;
60
+ setMounted(false);
61
+ return;
62
+ }
63
+ opacity.value = withTiming(0, { duration }, (finished) => {
64
+ if (finished) runOnJS(setMounted)(false);
65
+ });
66
+ }, [loading, reducedMotion, duration, opacity]);
67
+
68
+ return { mounted, opacity };
69
+ }
package/src/index.ts CHANGED
@@ -744,6 +744,7 @@ export {
744
744
  export {
745
745
  Marquee,
746
746
  type MarqueeProps,
747
+ type MarqueeGroupProps,
747
748
  type MarqueeDirection,
748
749
  } from './components/marquee';
749
750
  export {
@@ -843,6 +844,14 @@ export {
843
844
  type PostVote,
844
845
  } from './components/post';
845
846
  export { Progress, type ProgressProps } from './components/progress';
847
+ export {
848
+ ProgressButton,
849
+ useProgressButton,
850
+ type ProgressButtonProps,
851
+ type ProgressButtonLabelProps,
852
+ type ProgressButtonVariant,
853
+ type ProgressButtonSize,
854
+ } from './components/progress-button';
846
855
  export {
847
856
  QRCode,
848
857
  type QRCodeProps,
@@ -1070,6 +1079,7 @@ export {
1070
1079
  type SplitViewPaneProps,
1071
1080
  type SplitViewDragAreaProps,
1072
1081
  type SplitViewHandleProps,
1082
+ type SplitViewVariant,
1073
1083
  } from './components/split-view';
1074
1084
  export {
1075
1085
  Steps,
@@ -39,6 +39,23 @@ interface NativeUIModule {
39
39
  * height is reported back.
40
40
  */
41
41
  matchContents?: boolean | { vertical?: boolean; horizontal?: boolean };
42
+ /**
43
+ * Which safe areas the host lets the platform inset its content for.
44
+ *
45
+ * Every host here passes `keyboard`, and it is not optional. A host is a
46
+ * hosting controller, and a hosting controller insets its content for the
47
+ * keyboard by default — so a control docked above the keyboard has its
48
+ * content moved *inside* the box React Native gave it, over whatever is
49
+ * above it, while React Native's own layout says nothing has moved. That
50
+ * is a composer whose buttons sit on its text until the next layout pass
51
+ * puts them back.
52
+ *
53
+ * React Native owns the layout of everything in this library, so a
54
+ * platform that moves content inside a box we positioned is never right.
55
+ * `keyboard` and not `all`: the notch and the home indicator are the
56
+ * platform's business and stay its business.
57
+ */
58
+ ignoreSafeArea?: 'all' | 'container' | 'keyboard';
42
59
  style?: unknown;
43
60
  [key: string]: unknown;
44
61
  }>;