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.
- package/lib/module/components/ai-input/index.js +23 -7
- package/lib/module/components/ai-input/index.js.map +1 -1
- package/lib/module/components/area-chart/index.js +32 -25
- package/lib/module/components/area-chart/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +98 -15
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +7 -1
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/funnel-chart/index.js +15 -2
- package/lib/module/components/funnel-chart/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +19 -27
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/hex-chart/index.js +2 -0
- package/lib/module/components/hex-chart/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +38 -31
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/marquee/index.js +91 -5
- package/lib/module/components/marquee/index.js.map +1 -1
- package/lib/module/components/pie-chart/index.js +15 -2
- package/lib/module/components/pie-chart/index.js.map +1 -1
- package/lib/module/components/plot/index.js +42 -44
- package/lib/module/components/plot/index.js.map +1 -1
- package/lib/module/components/progress-button/index.js +390 -0
- package/lib/module/components/progress-button/index.js.map +1 -0
- package/lib/module/components/progress-button/progress-button-hold.js +57 -0
- package/lib/module/components/progress-button/progress-button-hold.js.map +1 -0
- package/lib/module/components/scatter-chart/index.js +6 -25
- package/lib/module/components/scatter-chart/index.js.map +1 -1
- package/lib/module/components/section-rail/index.js +40 -17
- package/lib/module/components/section-rail/index.js.map +1 -1
- package/lib/module/components/select/index.js +1 -0
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/slider/index.js +1 -0
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/split-view/index.js +54 -10
- package/lib/module/components/split-view/index.js.map +1 -1
- package/lib/module/components/switch/index.js +1 -0
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/timeline/index.js +145 -14
- package/lib/module/components/timeline/index.js.map +1 -1
- package/lib/module/components/toast/index.js +45 -11
- package/lib/module/components/toast/index.js.map +1 -1
- package/lib/module/components/treemap-chart/index.js +63 -6
- package/lib/module/components/treemap-chart/index.js.map +1 -1
- package/lib/module/components/waterfall-chart/index.js +20 -4
- package/lib/module/components/waterfall-chart/index.js.map +1 -1
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-scroll-sections.js +44 -2
- package/lib/module/hooks/use-scroll-sections.js.map +1 -1
- package/lib/module/hooks/use-skeleton-handoff.js +58 -0
- package/lib/module/hooks/use-skeleton-handoff.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js.map +1 -1
- package/lib/typescript/src/components/ai-input/index.d.ts +24 -5
- package/lib/typescript/src/components/ai-input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +15 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/funnel-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/marquee/index.d.ts +55 -2
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
- package/lib/typescript/src/components/pie-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/plot/index.d.ts +1 -3
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
- package/lib/typescript/src/components/progress-button/index.d.ts +295 -0
- package/lib/typescript/src/components/progress-button/index.d.ts.map +1 -0
- package/lib/typescript/src/components/progress-button/progress-button-hold.d.ts +40 -0
- package/lib/typescript/src/components/progress-button/progress-button-hold.d.ts.map +1 -0
- package/lib/typescript/src/components/scatter-chart/index.d.ts +1 -1
- package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
- package/lib/typescript/src/components/split-view/index.d.ts +10 -1
- package/lib/typescript/src/components/split-view/index.d.ts.map +1 -1
- package/lib/typescript/src/components/timeline/index.d.ts +7 -0
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
- package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
- package/lib/typescript/src/components/treemap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +1 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts +1 -0
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-skeleton-handoff.d.ts +11 -0
- package/lib/typescript/src/hooks/use-skeleton-handoff.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +17 -0
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/ai-input/index.tsx +46 -12
- package/src/components/area-chart/index.tsx +49 -37
- package/src/components/bottom-sheet/index.tsx +110 -12
- package/src/components/button/index.tsx +20 -1
- package/src/components/funnel-chart/index.tsx +10 -2
- package/src/components/heatmap-chart/index.tsx +27 -22
- package/src/components/hex-chart/index.tsx +22 -6
- package/src/components/line-chart/index.tsx +57 -43
- package/src/components/marquee/index.tsx +129 -6
- package/src/components/pie-chart/index.tsx +10 -2
- package/src/components/plot/index.tsx +66 -50
- package/src/components/progress-button/index.tsx +465 -0
- package/src/components/progress-button/progress-button-hold.ts +57 -0
- package/src/components/scatter-chart/index.tsx +4 -25
- package/src/components/section-rail/index.tsx +39 -19
- package/src/components/select/index.tsx +1 -1
- package/src/components/slider/index.tsx +1 -1
- package/src/components/split-view/index.tsx +53 -8
- package/src/components/switch/index.tsx +1 -1
- package/src/components/timeline/index.tsx +143 -17
- package/src/components/toast/index.tsx +60 -11
- package/src/components/treemap-chart/index.tsx +65 -8
- package/src/components/waterfall-chart/index.tsx +14 -4
- package/src/hooks/index.ts +5 -0
- package/src/hooks/use-scroll-sections.ts +52 -3
- package/src/hooks/use-skeleton-handoff.ts +69 -0
- package/src/index.ts +10 -0
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
866
|
+
if (!mounted || width <= 0 || height <= 0) return null;
|
|
849
867
|
|
|
850
868
|
return (
|
|
851
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
-
|
|
1047
|
+
if (!mounted || !d) return null;
|
|
1038
1048
|
|
|
1039
1049
|
return (
|
|
1040
|
-
<
|
|
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
|
-
</
|
|
1059
|
+
</AnimatedG>
|
|
1050
1060
|
);
|
|
1051
1061
|
}
|
|
1052
1062
|
WaterfallChartSkeleton.displayName = 'WaterfallChart.Skeleton';
|
package/src/hooks/index.ts
CHANGED
|
@@ -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,
|
package/src/native/index.ts
CHANGED
|
@@ -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
|
}>;
|