panelui-native 0.13.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -23
- package/lib/module/components/button/index.js +12 -9
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/chip/index.js +313 -0
- package/lib/module/components/chip/index.js.map +1 -0
- package/lib/module/components/direction/index.js +86 -0
- package/lib/module/components/direction/index.js.map +1 -0
- package/lib/module/components/frame/index.js +41 -39
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +772 -0
- package/lib/module/components/heatmap-chart/index.js.map +1 -0
- package/lib/module/components/input/index.js +4 -0
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/message/index.js +16 -1
- package/lib/module/components/message/index.js.map +1 -1
- package/lib/module/components/scroll-canvas/index.js +175 -0
- package/lib/module/components/scroll-canvas/index.js.map +1 -0
- package/lib/module/components/scroll-text/index.js +213 -0
- package/lib/module/components/scroll-text/index.js.map +1 -0
- package/lib/module/components/select/index.js +20 -24
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/slider/index.js +2 -5
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/soundwave/index.js +813 -0
- package/lib/module/components/soundwave/index.js.map +1 -0
- package/lib/module/components/switch/index.js +15 -17
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/thinking-orb/index.js +741 -0
- package/lib/module/components/thinking-orb/index.js.map +1 -0
- package/lib/module/components/toggle-button/index.js +12 -3
- package/lib/module/components/toggle-button/index.js.map +1 -1
- package/lib/module/components/typography/index.js +196 -5
- package/lib/module/components/typography/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-keyboard-avoidance.js +99 -59
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/module/hooks/use-reveal-progress.js +75 -0
- package/lib/module/hooks/use-reveal-progress.js.map +1 -0
- package/lib/module/icons/index.js +69 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +9 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/primitives/keyboard-avoider.js +21 -2
- package/lib/module/primitives/keyboard-avoider.js.map +1 -1
- package/lib/module/primitives/scroll-progress.js +114 -0
- package/lib/module/primitives/scroll-progress.js.map +1 -0
- package/lib/typescript/src/components/button/index.d.ts +3 -4
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/chip/index.d.ts +304 -0
- package/lib/typescript/src/components/chip/index.d.ts.map +1 -0
- package/lib/typescript/src/components/direction/index.d.ts +64 -0
- package/lib/typescript/src/components/direction/index.d.ts.map +1 -0
- package/lib/typescript/src/components/frame/index.d.ts +15 -13
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +260 -0
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/input/index.d.ts +19 -4
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message/index.d.ts.map +1 -1
- package/lib/typescript/src/components/scroll-canvas/index.d.ts +66 -0
- package/lib/typescript/src/components/scroll-canvas/index.d.ts.map +1 -0
- package/lib/typescript/src/components/scroll-text/index.d.ts +46 -0
- package/lib/typescript/src/components/scroll-text/index.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/soundwave/index.d.ts +91 -0
- package/lib/typescript/src/components/soundwave/index.d.ts.map +1 -0
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/thinking-orb/index.d.ts +28 -0
- package/lib/typescript/src/components/thinking-orb/index.d.ts.map +1 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts +12 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/typography/index.d.ts +154 -1
- package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +2 -1
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +16 -3
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts +56 -0
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +6 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +9 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +15 -7
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts +30 -3
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
- package/lib/typescript/src/primitives/scroll-progress.d.ts +61 -0
- package/lib/typescript/src/primitives/scroll-progress.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/button/index.tsx +13 -11
- package/src/components/direction/index.tsx +90 -0
- package/src/components/frame/index.tsx +39 -50
- package/src/components/heatmap-chart/index.tsx +1024 -0
- package/src/components/input/index.tsx +23 -4
- package/src/components/message/index.tsx +17 -1
- package/src/components/scroll-canvas/index.tsx +220 -0
- package/src/components/scroll-text/index.tsx +277 -0
- package/src/components/select/index.tsx +4 -9
- package/src/components/slider/index.tsx +4 -7
- package/src/components/soundwave/index.tsx +1029 -0
- package/src/components/switch/index.tsx +5 -8
- package/src/components/thinking-orb/index.tsx +792 -0
- package/src/components/typography/index.tsx +231 -10
- package/src/hooks/index.ts +6 -0
- package/src/hooks/use-keyboard-avoidance.ts +111 -65
- package/src/hooks/use-reveal-progress.ts +106 -0
- package/src/icons/index.tsx +36 -0
- package/src/index.ts +53 -0
- package/src/native/index.ts +15 -7
- package/src/primitives/keyboard-avoider.tsx +41 -4
- package/src/primitives/scroll-progress.tsx +167 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useRevealProgress — how far an element has travelled through the viewport.
|
|
3
|
+
*
|
|
4
|
+
* The number every scroll-driven effect is really made of: `0` before the
|
|
5
|
+
* element has arrived, `1` once it has passed, and a smooth scrub between the
|
|
6
|
+
* two. What an effect *does* with it — recolour, fade, zoom, step through
|
|
7
|
+
* frames — is the only part that differs.
|
|
8
|
+
*
|
|
9
|
+
* The window is set the way a scroll-trigger's is: `start` and `end` are
|
|
10
|
+
* positions down the viewport, as fractions of its height. Progress is `0`
|
|
11
|
+
* while the element's **top** is still below `start`, and reaches `1` when its
|
|
12
|
+
* **bottom** passes `end` — so a tall block scrubs across its own height rather
|
|
13
|
+
* than snapping the moment its first line appears.
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const { ref, progress } = useRevealProgress({ start: 0.9, end: 0.5 });
|
|
17
|
+
*
|
|
18
|
+
* <Animated.View ref={ref} style={useAnimatedStyle(() => ({
|
|
19
|
+
* opacity: progress.value,
|
|
20
|
+
* }))} />
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The element is measured on the UI thread whenever the scroll position
|
|
24
|
+
* changes — not every frame, and not once at layout. Once is wrong the moment
|
|
25
|
+
* anything above it resizes; every frame is a measurement pass for a value that
|
|
26
|
+
* only changes when the scroller moves.
|
|
27
|
+
*/
|
|
28
|
+
import { useWindowDimensions, type View } from 'react-native';
|
|
29
|
+
import {
|
|
30
|
+
measure,
|
|
31
|
+
useAnimatedRef,
|
|
32
|
+
useDerivedValue,
|
|
33
|
+
type AnimatedRef,
|
|
34
|
+
type SharedValue,
|
|
35
|
+
} from 'react-native-reanimated';
|
|
36
|
+
import { useScrollProgress } from '../primitives/scroll-progress';
|
|
37
|
+
|
|
38
|
+
export interface UseRevealProgressOptions {
|
|
39
|
+
/**
|
|
40
|
+
* Where down the viewport the element's top sits when progress is `0`, as a
|
|
41
|
+
* fraction of the viewport height. `0.9` starts it just before it appears.
|
|
42
|
+
*/
|
|
43
|
+
start?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Where down the viewport the element's bottom sits when progress reaches
|
|
46
|
+
* `1`. Smaller values mean a longer scrub.
|
|
47
|
+
*/
|
|
48
|
+
end?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Drive the effect from a value of your own instead of from the scroll
|
|
51
|
+
* position. Anything passed here is used as-is and nothing is measured.
|
|
52
|
+
*/
|
|
53
|
+
progress?: SharedValue<number>;
|
|
54
|
+
/** Set false to hold progress at `1`, as though the element had passed. */
|
|
55
|
+
enabled?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface UseRevealProgressResult {
|
|
59
|
+
/** Attach to the element whose travel drives the effect. */
|
|
60
|
+
ref: AnimatedRef<View>;
|
|
61
|
+
/** `0` before, `1` after, scrubbed between. */
|
|
62
|
+
progress: SharedValue<number>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function useRevealProgress({
|
|
66
|
+
start = 0.9,
|
|
67
|
+
end = 0.5,
|
|
68
|
+
progress: external,
|
|
69
|
+
enabled = true,
|
|
70
|
+
}: UseRevealProgressOptions = {}): UseRevealProgressResult {
|
|
71
|
+
const ref = useAnimatedRef<View>();
|
|
72
|
+
const scroll = useScrollProgress();
|
|
73
|
+
const { height: windowHeight } = useWindowDimensions();
|
|
74
|
+
|
|
75
|
+
const offset = scroll?.offset;
|
|
76
|
+
const viewportValue = scroll?.viewport;
|
|
77
|
+
const topValue = scroll?.top;
|
|
78
|
+
|
|
79
|
+
const derived = useDerivedValue(() => {
|
|
80
|
+
if (!enabled) return 1;
|
|
81
|
+
|
|
82
|
+
// Read so the scroll position is a real dependency of this value. The
|
|
83
|
+
// element's own position is not observable, and the scroller moving
|
|
84
|
+
// underneath it is the only thing that changes it while it is on screen.
|
|
85
|
+
const scrolled = offset ? offset.value : 0;
|
|
86
|
+
const viewport = viewportValue?.value || windowHeight;
|
|
87
|
+
const viewportTop = topValue?.value ?? 0;
|
|
88
|
+
if (viewport <= 0 || Number.isNaN(scrolled)) return 0;
|
|
89
|
+
|
|
90
|
+
const frame = measure(ref);
|
|
91
|
+
if (!frame || frame.height <= 0) return 0;
|
|
92
|
+
|
|
93
|
+
const top = frame.pageY - viewportTop;
|
|
94
|
+
const from = start * viewport;
|
|
95
|
+
const to = end * viewport - frame.height;
|
|
96
|
+
const span = from - to;
|
|
97
|
+
if (span <= 0) return top <= to ? 1 : 0;
|
|
98
|
+
|
|
99
|
+
const value = (from - top) / span;
|
|
100
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// An external value is passed straight through, so a caller driving the
|
|
104
|
+
// effect by hand never pays for the measurement.
|
|
105
|
+
return { ref, progress: external ?? derived };
|
|
106
|
+
}
|
package/src/icons/index.tsx
CHANGED
|
@@ -510,3 +510,39 @@ export function PaperclipIcon({ size = 16, color, ...props }: IconProps) {
|
|
|
510
510
|
</Svg>
|
|
511
511
|
);
|
|
512
512
|
}
|
|
513
|
+
|
|
514
|
+
/** A capsule on a stand — a microphone, for anything voice. */
|
|
515
|
+
export function MicIcon({ size = 16, color, ...props }: IconProps) {
|
|
516
|
+
const resolved = useResolvedColor(color, '#737373');
|
|
517
|
+
return (
|
|
518
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
519
|
+
<Path
|
|
520
|
+
d="M12 3a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3zM5 11v1a7 7 0 0 0 14 0v-1M12 19v3"
|
|
521
|
+
stroke={resolved}
|
|
522
|
+
strokeWidth={2}
|
|
523
|
+
strokeLinecap="round"
|
|
524
|
+
strokeLinejoin="round"
|
|
525
|
+
/>
|
|
526
|
+
</Svg>
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** A filled triangle — start playback. */
|
|
531
|
+
export function PlayIcon({ size = 16, color, ...props }: IconProps) {
|
|
532
|
+
const resolved = useResolvedColor(color, '#737373');
|
|
533
|
+
return (
|
|
534
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
535
|
+
<Path d="M7 4.5v15l13-7.5z" fill={resolved} stroke={resolved} strokeWidth={2} strokeLinejoin="round" />
|
|
536
|
+
</Svg>
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Two filled bars — hold playback where it is. */
|
|
541
|
+
export function PauseIcon({ size = 16, color, ...props }: IconProps) {
|
|
542
|
+
const resolved = useResolvedColor(color, '#737373');
|
|
543
|
+
return (
|
|
544
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
545
|
+
<Path d="M8 5h2v14H8zM14 5h2v14h-2z" fill={resolved} stroke={resolved} strokeWidth={1.5} strokeLinejoin="round" />
|
|
546
|
+
</Svg>
|
|
547
|
+
);
|
|
548
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,12 @@ export {
|
|
|
25
25
|
AnimatedPressable,
|
|
26
26
|
type AnimatedPressableProps,
|
|
27
27
|
} from './primitives/animated-pressable';
|
|
28
|
+
export {
|
|
29
|
+
ScrollProgress,
|
|
30
|
+
useScrollProgress,
|
|
31
|
+
type ScrollProgressProps,
|
|
32
|
+
type ScrollProgressValue,
|
|
33
|
+
} from './primitives/scroll-progress';
|
|
28
34
|
|
|
29
35
|
// Native UI bridge
|
|
30
36
|
export { hasNativeUI } from './native';
|
|
@@ -86,6 +92,18 @@ export {
|
|
|
86
92
|
type DialogProps,
|
|
87
93
|
type DialogContentProps,
|
|
88
94
|
} from './components/dialog';
|
|
95
|
+
export {
|
|
96
|
+
Soundwave,
|
|
97
|
+
type SoundwaveProps,
|
|
98
|
+
type SoundwaveState,
|
|
99
|
+
type SoundwaveVariant,
|
|
100
|
+
} from './components/soundwave';
|
|
101
|
+
export {
|
|
102
|
+
Direction,
|
|
103
|
+
useDirection,
|
|
104
|
+
type DirectionProps,
|
|
105
|
+
type DirectionValue,
|
|
106
|
+
} from './components/direction';
|
|
89
107
|
export {
|
|
90
108
|
Frame,
|
|
91
109
|
type FrameProps,
|
|
@@ -95,6 +113,22 @@ export {
|
|
|
95
113
|
type FrameSectionProps,
|
|
96
114
|
type FrameVariant,
|
|
97
115
|
} from './components/frame';
|
|
116
|
+
export {
|
|
117
|
+
HeatmapChart,
|
|
118
|
+
useHeatmapChart,
|
|
119
|
+
buildHeatmapCalendar,
|
|
120
|
+
type HeatmapChartProps,
|
|
121
|
+
type HeatmapCellsProps,
|
|
122
|
+
type HeatmapSeparatorProps,
|
|
123
|
+
type HeatmapXAxisProps,
|
|
124
|
+
type HeatmapYAxisProps,
|
|
125
|
+
type HeatmapTooltipProps,
|
|
126
|
+
type HeatmapLegendProps,
|
|
127
|
+
type HeatmapColumn,
|
|
128
|
+
type HeatmapBin,
|
|
129
|
+
type HeatmapCell,
|
|
130
|
+
type HeatmapLayout,
|
|
131
|
+
} from './components/heatmap-chart';
|
|
98
132
|
export {
|
|
99
133
|
Select,
|
|
100
134
|
type SelectProps,
|
|
@@ -222,6 +256,22 @@ export {
|
|
|
222
256
|
} from './components/scroll-fade';
|
|
223
257
|
export { Separator, type SeparatorProps } from './components/separator';
|
|
224
258
|
export { Shimmer, type ShimmerProps } from './components/shimmer';
|
|
259
|
+
export {
|
|
260
|
+
ScrollText,
|
|
261
|
+
type ScrollTextProps,
|
|
262
|
+
type ScrollTextEffect,
|
|
263
|
+
type ScrollTextSplit,
|
|
264
|
+
} from './components/scroll-text';
|
|
265
|
+
export {
|
|
266
|
+
ScrollCanvas,
|
|
267
|
+
type ScrollCanvasProps,
|
|
268
|
+
type ScrollCanvasEffect,
|
|
269
|
+
} from './components/scroll-canvas';
|
|
270
|
+
export {
|
|
271
|
+
ThinkingOrb,
|
|
272
|
+
type ThinkingOrbProps,
|
|
273
|
+
type ThinkingOrbState,
|
|
274
|
+
} from './components/thinking-orb';
|
|
225
275
|
export { Skeleton, type SkeletonProps } from './components/skeleton';
|
|
226
276
|
export { Slider, type SliderProps } from './components/slider';
|
|
227
277
|
export { Spinner, type SpinnerProps } from './components/spinner';
|
|
@@ -287,9 +337,12 @@ export {
|
|
|
287
337
|
IconColorProvider,
|
|
288
338
|
InfoIcon,
|
|
289
339
|
ImageIcon,
|
|
340
|
+
MicIcon,
|
|
290
341
|
MoonIcon,
|
|
291
342
|
PackageIcon,
|
|
292
343
|
PaperclipIcon,
|
|
344
|
+
PauseIcon,
|
|
345
|
+
PlayIcon,
|
|
293
346
|
PlusSquareIcon,
|
|
294
347
|
ReceiptIcon,
|
|
295
348
|
SearchIcon,
|
package/src/native/index.ts
CHANGED
|
@@ -28,13 +28,15 @@ interface NativeUIModule {
|
|
|
28
28
|
/**
|
|
29
29
|
* Whether the host resizes itself to the platform content.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* is a
|
|
31
|
+
* This is on for every control here, and it is the whole answer to the
|
|
32
|
+
* jump. Sizing the *host* and leaving the control unsized inside it hands
|
|
33
|
+
* the platform a box it never agreed to: it lays out against its own
|
|
34
|
+
* intrinsic size, and settles into the box on the first thing that forces
|
|
35
|
+
* a second pass — which for a button is the first press.
|
|
36
|
+
*
|
|
37
|
+
* The per-axis form is for a control with no intrinsic width, like a
|
|
38
|
+
* slider or a picker: the width comes from ordinary layout and only the
|
|
39
|
+
* height is reported back.
|
|
38
40
|
*/
|
|
39
41
|
matchContents?: boolean | { vertical?: boolean; horizontal?: boolean };
|
|
40
42
|
style?: unknown;
|
|
@@ -50,6 +52,12 @@ interface NativeUIModule {
|
|
|
50
52
|
matchContents?: boolean;
|
|
51
53
|
style?: unknown;
|
|
52
54
|
}>;
|
|
55
|
+
/**
|
|
56
|
+
* `style` here is not a React Native style — it is the small portable subset
|
|
57
|
+
* (`width`, `height`, padding, `backgroundColor`, `borderRadius`, `opacity`)
|
|
58
|
+
* that the toolkit compiles into real SwiftUI and Compose modifiers. It is
|
|
59
|
+
* how a control is given a definite size without the host having to guess.
|
|
60
|
+
*/
|
|
53
61
|
Button: ComponentType<Record<string, unknown>>;
|
|
54
62
|
Switch: ComponentType<Record<string, unknown>>;
|
|
55
63
|
Slider: ComponentType<Record<string, unknown>>;
|
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* KeyboardAvoider — a view that
|
|
2
|
+
* KeyboardAvoider — a view that keeps itself clear of the keyboard.
|
|
3
3
|
*
|
|
4
4
|
* A thin wrapper over `useKeyboardAvoidance` for the common case of "keep this
|
|
5
5
|
* box visible", so a form does not have to wire the ref, layout handler and
|
|
6
6
|
* animated style by hand.
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // In the flow of a scrolling page: lifts by the overlap, follows the scroll.
|
|
10
|
+
* <KeyboardAvoider active={focused}>…</KeyboardAvoider>
|
|
11
|
+
*
|
|
12
|
+
* // Pinned to the bottom edge: rides the keyboard up and back down.
|
|
13
|
+
* <KeyboardAvoider
|
|
14
|
+
* mode="dock"
|
|
15
|
+
* bottomInset={insets.bottom}
|
|
16
|
+
* className="absolute left-0 right-0"
|
|
17
|
+
* style={{ bottom: insets.bottom + 16 }}
|
|
18
|
+
* >
|
|
19
|
+
* …
|
|
20
|
+
* </KeyboardAvoider>
|
|
21
|
+
* ```
|
|
7
22
|
*/
|
|
8
23
|
import type { ReactNode } from 'react';
|
|
9
24
|
import type { ViewProps } from 'react-native';
|
|
10
25
|
import Animated from 'react-native-reanimated';
|
|
11
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
useKeyboardAvoidance,
|
|
28
|
+
type KeyboardAvoidanceMode,
|
|
29
|
+
} from '../hooks/use-keyboard-avoidance';
|
|
12
30
|
|
|
13
31
|
export interface KeyboardAvoiderProps extends ViewProps {
|
|
14
32
|
className?: string;
|
|
@@ -21,8 +39,19 @@ export interface KeyboardAvoiderProps extends ViewProps {
|
|
|
21
39
|
* screen stay put.
|
|
22
40
|
*/
|
|
23
41
|
active?: boolean;
|
|
24
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* `lift` moves in-flow content up by its overlap with the keyboard and keeps
|
|
44
|
+
* tracking it as the page scrolls. `dock` travels with the keyboard, for a
|
|
45
|
+
* bar already pinned near the bottom edge.
|
|
46
|
+
*/
|
|
47
|
+
mode?: KeyboardAvoidanceMode;
|
|
48
|
+
/** Gap to keep between the content's bottom edge and the keyboard. `lift` only. */
|
|
25
49
|
offset?: number;
|
|
50
|
+
/**
|
|
51
|
+
* How far above the bottom edge the bar already sits — usually the safe area
|
|
52
|
+
* inset it is positioned by. `dock` only.
|
|
53
|
+
*/
|
|
54
|
+
bottomInset?: number;
|
|
26
55
|
children?: ReactNode;
|
|
27
56
|
}
|
|
28
57
|
|
|
@@ -30,13 +59,21 @@ export function KeyboardAvoider({
|
|
|
30
59
|
className,
|
|
31
60
|
enabled = true,
|
|
32
61
|
active = true,
|
|
62
|
+
mode = 'lift',
|
|
33
63
|
offset = 16,
|
|
64
|
+
bottomInset = 0,
|
|
34
65
|
children,
|
|
35
66
|
style,
|
|
36
67
|
onLayout,
|
|
37
68
|
...props
|
|
38
69
|
}: KeyboardAvoiderProps) {
|
|
39
|
-
const avoidance = useKeyboardAvoidance({
|
|
70
|
+
const avoidance = useKeyboardAvoidance({
|
|
71
|
+
enabled,
|
|
72
|
+
active,
|
|
73
|
+
mode,
|
|
74
|
+
offset,
|
|
75
|
+
bottomInset,
|
|
76
|
+
});
|
|
40
77
|
|
|
41
78
|
return (
|
|
42
79
|
<Animated.View
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScrollProgress — publishes a scroll container's position so children can
|
|
3
|
+
* animate against it.
|
|
4
|
+
*
|
|
5
|
+
* Scroll-driven effects all need the same two numbers: where the scroller is,
|
|
6
|
+
* and how tall its viewport is. Every component that wanted them measuring for
|
|
7
|
+
* itself would mean one scroll listener per effect and one measurement pass per
|
|
8
|
+
* effect per frame, for two values that are the same for all of them.
|
|
9
|
+
*
|
|
10
|
+
* It wraps the scroll view you already have rather than replacing it — the
|
|
11
|
+
* child is cloned with an animated scroll handler composed onto it, the same
|
|
12
|
+
* way `ScrollFade` does — so a `FlatList`, a `SectionList` or your own
|
|
13
|
+
* scrollable all work, and nothing has to be rewritten to adopt it.
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <ScrollProgress>
|
|
17
|
+
* <ScrollView>
|
|
18
|
+
* <ScrollText>…</ScrollText>
|
|
19
|
+
* </ScrollView>
|
|
20
|
+
* </ScrollProgress>
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Both values are shared values, so the whole chain from scroll event to
|
|
24
|
+
* animated style stays on the UI thread and nothing re-renders as you scroll.
|
|
25
|
+
*/
|
|
26
|
+
import {
|
|
27
|
+
Children,
|
|
28
|
+
createContext,
|
|
29
|
+
isValidElement,
|
|
30
|
+
useContext,
|
|
31
|
+
useMemo,
|
|
32
|
+
useRef,
|
|
33
|
+
type ComponentType,
|
|
34
|
+
type ReactElement,
|
|
35
|
+
type ReactNode,
|
|
36
|
+
} from 'react';
|
|
37
|
+
import { View, type LayoutChangeEvent, type ViewProps } from 'react-native';
|
|
38
|
+
import Animated, {
|
|
39
|
+
useAnimatedScrollHandler,
|
|
40
|
+
useComposedEventHandler,
|
|
41
|
+
useSharedValue,
|
|
42
|
+
type AnimatedScrollViewProps,
|
|
43
|
+
type SharedValue,
|
|
44
|
+
} from 'react-native-reanimated';
|
|
45
|
+
|
|
46
|
+
export interface ScrollProgressValue {
|
|
47
|
+
/** Distance scrolled, in pixels. */
|
|
48
|
+
offset: SharedValue<number>;
|
|
49
|
+
/** Height of the visible area. */
|
|
50
|
+
viewport: SharedValue<number>;
|
|
51
|
+
/** Total height of the content. */
|
|
52
|
+
content: SharedValue<number>;
|
|
53
|
+
/**
|
|
54
|
+
* Window-space top edge of the scroller. What turns an element's measured
|
|
55
|
+
* `pageY` into a position inside *this* viewport rather than the screen's —
|
|
56
|
+
* they differ by whatever sits above the scroller, and assuming they do not
|
|
57
|
+
* is why scroll effects drift under a header.
|
|
58
|
+
*/
|
|
59
|
+
top: SharedValue<number>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ScrollProgressContext = createContext<ScrollProgressValue | null>(null);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The nearest enclosing scroll container's position, or `null` outside one.
|
|
66
|
+
*
|
|
67
|
+
* It returns null rather than throwing because every consumer of this also
|
|
68
|
+
* takes an explicit `progress` prop — a component that can be driven by hand
|
|
69
|
+
* should not insist on a provider it does not need.
|
|
70
|
+
*/
|
|
71
|
+
export function useScrollProgress(): ScrollProgressValue | null {
|
|
72
|
+
return useContext(ScrollProgressContext);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ScrollableProps {
|
|
76
|
+
onScroll?: AnimatedScrollViewProps['onScroll'];
|
|
77
|
+
onLayout?: (event: LayoutChangeEvent) => void;
|
|
78
|
+
onContentSizeChange?: (width: number, height: number) => void;
|
|
79
|
+
scrollEventThrottle?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ScrollProgressProps extends ViewProps {
|
|
83
|
+
className?: string;
|
|
84
|
+
/** Exactly one scrollable — a ScrollView, FlatList, or anything like them. */
|
|
85
|
+
children?: ReactNode;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function ScrollProgress({ className, children, ...props }: ScrollProgressProps) {
|
|
89
|
+
const offset = useSharedValue(0);
|
|
90
|
+
const viewport = useSharedValue(0);
|
|
91
|
+
const content = useSharedValue(0);
|
|
92
|
+
const top = useSharedValue(0);
|
|
93
|
+
const hostRef = useRef<View>(null);
|
|
94
|
+
|
|
95
|
+
const child = Children.only(children) as ReactElement<ScrollableProps>;
|
|
96
|
+
|
|
97
|
+
// Reanimated can only drive a scroll handler on an animated component, and
|
|
98
|
+
// what was passed in is not necessarily `Animated.ScrollView`. Keyed on the
|
|
99
|
+
// element *type*, not the element: rebuilding the wrapper would remount the
|
|
100
|
+
// list and lose its scroll position.
|
|
101
|
+
const childType = isValidElement(child)
|
|
102
|
+
? (child.type as ComponentType<ScrollableProps>)
|
|
103
|
+
: null;
|
|
104
|
+
const AnimatedScrollable = useMemo(
|
|
105
|
+
() => (childType ? Animated.createAnimatedComponent(childType) : null),
|
|
106
|
+
[childType]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const scrollHandler = useAnimatedScrollHandler({
|
|
110
|
+
onScroll: (event) => {
|
|
111
|
+
offset.value = event.contentOffset.y;
|
|
112
|
+
viewport.value = event.layoutMeasurement.height;
|
|
113
|
+
content.value = event.contentSize.height;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// A consumer's own `onScroll` is composed rather than dropped — but since the
|
|
118
|
+
// child is now an animated component, it has to be an animated handler too.
|
|
119
|
+
const onScroll = useComposedEventHandler([
|
|
120
|
+
scrollHandler,
|
|
121
|
+
(child.props.onScroll as typeof scrollHandler | undefined) ?? null,
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
// Taken up front as well as on scroll, so anything already on screen animates
|
|
125
|
+
// from the first frame instead of waiting for the first scroll event.
|
|
126
|
+
const onLayout = (event: LayoutChangeEvent) => {
|
|
127
|
+
viewport.value = event.nativeEvent.layout.height;
|
|
128
|
+
// A frame later than onLayout on both platforms, because measureInWindow
|
|
129
|
+
// is only meaningful once the view is attached and positioned.
|
|
130
|
+
requestAnimationFrame(() => {
|
|
131
|
+
hostRef.current?.measureInWindow((_x, y) => {
|
|
132
|
+
if (y >= 0) top.value = y;
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
child.props.onLayout?.(event);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const onContentSizeChange = (width: number, height: number) => {
|
|
139
|
+
content.value = height;
|
|
140
|
+
child.props.onContentSizeChange?.(width, height);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const value = useMemo<ScrollProgressValue>(
|
|
144
|
+
() => ({ offset, viewport, content, top }),
|
|
145
|
+
[offset, viewport, content, top]
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<ScrollProgressContext.Provider value={value}>
|
|
150
|
+
<View {...props} ref={hostRef} className={className}>
|
|
151
|
+
{AnimatedScrollable && isValidElement(child) ? (
|
|
152
|
+
<AnimatedScrollable
|
|
153
|
+
{...child.props}
|
|
154
|
+
onScroll={onScroll}
|
|
155
|
+
onLayout={onLayout}
|
|
156
|
+
onContentSizeChange={onContentSizeChange}
|
|
157
|
+
scrollEventThrottle={child.props.scrollEventThrottle ?? 16}
|
|
158
|
+
/>
|
|
159
|
+
) : (
|
|
160
|
+
child
|
|
161
|
+
)}
|
|
162
|
+
</View>
|
|
163
|
+
</ScrollProgressContext.Provider>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
ScrollProgress.displayName = 'ScrollProgress';
|