panelui-native 0.77.0 → 0.79.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/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 +97 -15
- package/lib/module/components/bottom-sheet/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/map/index.js +63 -39
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/map/map-children.js +54 -0
- package/lib/module/components/map/map-children.js.map +1 -0
- package/lib/module/components/map/maplibre.js +31 -3
- package/lib/module/components/map/maplibre.js.map +1 -1
- package/lib/module/components/markdown-editor/index.js +288 -34
- package/lib/module/components/markdown-editor/index.js.map +1 -1
- package/lib/module/components/markdown-editor/markdown-transforms.js +272 -9
- package/lib/module/components/markdown-editor/markdown-transforms.js.map +1 -1
- package/lib/module/components/marquee/index.js +205 -46
- package/lib/module/components/marquee/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +162 -34
- package/lib/module/components/panelside/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 +617 -0
- package/lib/module/components/progress-button/index.js.map +1 -0
- package/lib/module/components/progress-button/progress-button-hold.js +91 -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/split-view/index.js +54 -10
- package/lib/module/components/split-view/index.js.map +1 -1
- package/lib/module/components/theme-selector/index.js +239 -0
- package/lib/module/components/theme-selector/index.js.map +1 -0
- package/lib/module/components/theme-selector/theme-preview.js +320 -0
- package/lib/module/components/theme-selector/theme-preview.js.map +1 -0
- 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 +2 -0
- package/lib/module/index.js.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/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/map/index.d.ts +8 -4
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/map-children.d.ts +41 -0
- package/lib/typescript/src/components/map/map-children.d.ts.map +1 -0
- package/lib/typescript/src/components/map/maplibre.d.ts +8 -4
- package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
- package/lib/typescript/src/components/markdown-editor/index.d.ts +64 -5
- package/lib/typescript/src/components/markdown-editor/index.d.ts.map +1 -1
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts +33 -5
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.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/panelside/index.d.ts +79 -4
- package/lib/typescript/src/components/panelside/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 +311 -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 +60 -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/theme-selector/index.d.ts +107 -0
- package/lib/typescript/src/components/theme-selector/index.d.ts.map +1 -0
- package/lib/typescript/src/components/theme-selector/theme-preview.d.ts +32 -0
- package/lib/typescript/src/components/theme-selector/theme-preview.d.ts.map +1 -0
- 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 +6 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/area-chart/index.tsx +49 -37
- package/src/components/bottom-sheet/index.tsx +109 -11
- 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/map/index.tsx +63 -36
- package/src/components/map/map-children.ts +66 -0
- package/src/components/map/maplibre.ts +32 -4
- package/src/components/markdown-editor/index.tsx +413 -35
- package/src/components/markdown-editor/markdown-transforms.ts +274 -10
- package/src/components/marquee/index.tsx +254 -58
- package/src/components/panelside/index.tsx +179 -40
- package/src/components/pie-chart/index.tsx +10 -2
- package/src/components/plot/index.tsx +66 -50
- package/src/components/progress-button/index.tsx +684 -0
- package/src/components/progress-button/progress-button-hold.ts +88 -0
- package/src/components/scatter-chart/index.tsx +4 -25
- package/src/components/section-rail/index.tsx +39 -19
- package/src/components/split-view/index.tsx +53 -8
- package/src/components/theme-selector/index.tsx +281 -0
- package/src/components/theme-selector/theme-preview.tsx +205 -0
- 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 +25 -0
|
@@ -11,17 +11,26 @@ import {
|
|
|
11
11
|
type ReactElement,
|
|
12
12
|
type ReactNode,
|
|
13
13
|
} from 'react';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
Pressable,
|
|
16
|
+
StyleSheet,
|
|
17
|
+
useWindowDimensions,
|
|
18
|
+
View,
|
|
19
|
+
type ViewProps,
|
|
20
|
+
} from 'react-native';
|
|
15
21
|
import { useCSSVariable } from 'uniwind';
|
|
16
22
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
17
23
|
import Animated, {
|
|
24
|
+
Extrapolation,
|
|
18
25
|
FadeIn,
|
|
26
|
+
interpolate,
|
|
19
27
|
FadeOut,
|
|
20
28
|
SlideInDown,
|
|
21
29
|
SlideOutDown,
|
|
22
30
|
runOnJS,
|
|
23
31
|
useAnimatedScrollHandler,
|
|
24
32
|
useAnimatedStyle,
|
|
33
|
+
useReducedMotion,
|
|
25
34
|
useSharedValue,
|
|
26
35
|
withSpring,
|
|
27
36
|
withTiming,
|
|
@@ -36,11 +45,60 @@ import { Scrim } from '../../primitives/scrim';
|
|
|
36
45
|
import { Text, textChildren } from '../../primitives/text';
|
|
37
46
|
import { useBackHandler } from '../../hooks/use-back-handler';
|
|
38
47
|
import { cn } from '../../utils/cn';
|
|
48
|
+
import { impactKnock } from '../../utils/haptics';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* One spring for the whole surface, in Apple's two designer parameters rather
|
|
52
|
+
* than mass/stiffness/damping — the sheet arriving, the sheet snapping back and
|
|
53
|
+
* the sheet leaving used to be three slightly different springs, which is three
|
|
54
|
+
* different weights for one object.
|
|
55
|
+
*
|
|
56
|
+
* A little under critically damped, so it settles without a bounce. A sheet
|
|
57
|
+
* that overshoots reads as light, and a sheet is not light.
|
|
58
|
+
*
|
|
59
|
+
* Slower than the 300ms a small state change gets, and deliberately. This is a
|
|
60
|
+
* large surface travelling most of the screen, and it is the same category of
|
|
61
|
+
* movement as a screen transition rather than as a toggle — the platform's own
|
|
62
|
+
* sheets are slower still. Taken at toggle speed it arrives before the eye has
|
|
63
|
+
* followed it, which reads as a jump-cut to a different screen rather than as
|
|
64
|
+
* something coming up from the bottom.
|
|
65
|
+
*/
|
|
66
|
+
const SPRING = { duration: 420, dampingRatio: 0.9 } as const;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Leaving, where the far side of the travel is off screen: clamped, because a
|
|
70
|
+
* spring allowed to overshoot past the bottom flashes a gap under the sheet on
|
|
71
|
+
* its way back.
|
|
72
|
+
*/
|
|
73
|
+
const EXIT_SPRING = { duration: 380, dampingRatio: 1, overshootClamping: true } as const;
|
|
39
74
|
|
|
40
|
-
const SPRING = { damping: 22, stiffness: 280, mass: 0.7 } as const;
|
|
41
75
|
const DISMISS_DISTANCE = 120;
|
|
42
76
|
const DISMISS_VELOCITY = 800;
|
|
43
77
|
|
|
78
|
+
/*
|
|
79
|
+
* Built once at module scope. A layout-animation builder constructed in render
|
|
80
|
+
* is a new object every commit, and the sheet re-renders while it is open.
|
|
81
|
+
*/
|
|
82
|
+
const ENTERING = SlideInDown.springify().dampingRatio(0.9).duration(420);
|
|
83
|
+
// Leaving is quicker than arriving, but not by much: the sheet is still
|
|
84
|
+
// crossing the whole screen, and a surface that big snapping out of existence
|
|
85
|
+
// is more startling than the delay it saves is worth.
|
|
86
|
+
const EXITING = SlideOutDown.springify().dampingRatio(1).duration(340);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Where a flick would come to rest if it kept decelerating — Apple's
|
|
90
|
+
* exponential-decay form.
|
|
91
|
+
*
|
|
92
|
+
* Distance alone makes the sheet heavy: a short fast flick downward is
|
|
93
|
+
* unmistakably a dismissal, and asking it to also travel 120 points first means
|
|
94
|
+
* the reader has to throw the sheet twice.
|
|
95
|
+
*/
|
|
96
|
+
function project(velocity: number): number {
|
|
97
|
+
'worklet';
|
|
98
|
+
const decelerationRate = 0.998;
|
|
99
|
+
return ((velocity / 1000) * decelerationRate) / (1 - decelerationRate);
|
|
100
|
+
}
|
|
101
|
+
|
|
44
102
|
const sheetVariants = tv({
|
|
45
103
|
base: 'border border-border bg-popover px-5 pt-2 shadow-lg',
|
|
46
104
|
variants: {
|
|
@@ -287,6 +345,7 @@ function BottomSheetContent({
|
|
|
287
345
|
const { open, setOpen } = context;
|
|
288
346
|
const nativeSheet = useContext(NativeSheetContext);
|
|
289
347
|
const { height: screenHeight, width: screenWidth } = useWindowDimensions();
|
|
348
|
+
const reducedMotion = useReducedMotion();
|
|
290
349
|
const insets = useSafeAreaInsets();
|
|
291
350
|
const translateY = useSharedValue(0);
|
|
292
351
|
const closeTint = useCSSVariable('--color-muted-foreground');
|
|
@@ -353,15 +412,31 @@ function BottomSheetContent({
|
|
|
353
412
|
translateY.value = next > 0 ? next : next / 3;
|
|
354
413
|
})
|
|
355
414
|
.onEnd((event) => {
|
|
415
|
+
/*
|
|
416
|
+
* Velocity decides as much as distance does, and it is handed to the
|
|
417
|
+
* animation rather than only consulted by it. A flat timing from
|
|
418
|
+
* wherever the finger let go ignores how fast it was moving, so the
|
|
419
|
+
* frame the touch ends is a visible seam between a fast drag and a
|
|
420
|
+
* slow slide — the single detail that separates a sheet that follows
|
|
421
|
+
* your hand from one that merely goes where you put it.
|
|
422
|
+
*/
|
|
423
|
+
const projected = translateY.value + project(event.velocityY);
|
|
356
424
|
if (
|
|
357
|
-
|
|
425
|
+
projected > DISMISS_DISTANCE ||
|
|
358
426
|
event.velocityY > DISMISS_VELOCITY
|
|
359
427
|
) {
|
|
360
|
-
translateY.value =
|
|
361
|
-
|
|
362
|
-
|
|
428
|
+
translateY.value = withSpring(
|
|
429
|
+
screenHeight,
|
|
430
|
+
{ ...EXIT_SPRING, velocity: event.velocityY },
|
|
431
|
+
(finished) => {
|
|
432
|
+
if (finished) runOnJS(close)();
|
|
433
|
+
}
|
|
434
|
+
);
|
|
363
435
|
} else {
|
|
364
|
-
translateY.value = withSpring(0, SPRING);
|
|
436
|
+
translateY.value = withSpring(0, { ...SPRING, velocity: event.velocityY });
|
|
437
|
+
// It caught rather than went. Fired here, at the moment the sheet
|
|
438
|
+
// commits to staying, not when it finishes arriving.
|
|
439
|
+
runOnJS(impactKnock)();
|
|
365
440
|
}
|
|
366
441
|
}),
|
|
367
442
|
// Rebuilt only when one of these changes. Built inline it would be a new
|
|
@@ -374,6 +449,15 @@ function BottomSheetContent({
|
|
|
374
449
|
transform: [{ translateY: translateY.value }],
|
|
375
450
|
}));
|
|
376
451
|
|
|
452
|
+
const backdropStyle = useAnimatedStyle(() => ({
|
|
453
|
+
opacity: interpolate(
|
|
454
|
+
translateY.value,
|
|
455
|
+
[0, screenHeight],
|
|
456
|
+
[1, 0],
|
|
457
|
+
Extrapolation.CLAMP
|
|
458
|
+
),
|
|
459
|
+
}));
|
|
460
|
+
|
|
377
461
|
/*
|
|
378
462
|
* A sized sheet is clamped rather than handed its fraction outright.
|
|
379
463
|
* `insets.top` is the status bar and the notch, and content that runs under
|
|
@@ -453,7 +537,12 @@ function BottomSheetContent({
|
|
|
453
537
|
subtree — re-provide the context so nested consumers keep working. */}
|
|
454
538
|
<BottomSheetContext.Provider value={context}>
|
|
455
539
|
<View className="absolute inset-0 justify-end">
|
|
456
|
-
|
|
540
|
+
{/* Derived from the same value the sheet moves on, so the backdrop
|
|
541
|
+
cannot disagree with it: dragging the sheet halfway down lightens
|
|
542
|
+
the screen behind it by half. Left to its own fade the scrim stayed
|
|
543
|
+
fully dark until the sheet had gone, which reads as a dimmed screen
|
|
544
|
+
with nothing on it. */}
|
|
545
|
+
<Animated.View style={[StyleSheet.absoluteFill, backdropStyle]}>
|
|
457
546
|
{/* Scrim draws the backdrop and its own fade; the Pressable over it
|
|
458
547
|
is what closes the sheet, since the scrim takes no touches. */}
|
|
459
548
|
<Scrim blur={blur} />
|
|
@@ -462,11 +551,20 @@ function BottomSheetContent({
|
|
|
462
551
|
className="flex-1"
|
|
463
552
|
onPress={dismissible ? close : undefined}
|
|
464
553
|
/>
|
|
465
|
-
</View>
|
|
554
|
+
</Animated.View>
|
|
466
555
|
<GestureDetector gesture={pan}>
|
|
467
556
|
<Animated.View
|
|
468
|
-
|
|
469
|
-
|
|
557
|
+
/*
|
|
558
|
+
* The same spring the drag settles with, so a sheet caught halfway
|
|
559
|
+
* through arriving and a sheet released after a drag are the same
|
|
560
|
+
* object moving at the same weight.
|
|
561
|
+
*
|
|
562
|
+
* Reduced motion keeps the scrim's fade and drops the slide: the
|
|
563
|
+
* state change still has to be legible, and it is the travel that
|
|
564
|
+
* setting is about.
|
|
565
|
+
*/
|
|
566
|
+
entering={reducedMotion ? FadeIn.duration(150) : ENTERING}
|
|
567
|
+
exiting={reducedMotion ? FadeOut.duration(150) : EXITING}
|
|
470
568
|
accessibilityViewIsModal
|
|
471
569
|
className={sheetVariants({ detached, className })}
|
|
472
570
|
{...props}
|
|
@@ -90,6 +90,7 @@ import { useDirection } from '../../hooks/use-direction';
|
|
|
90
90
|
import { Text } from '../../primitives/text';
|
|
91
91
|
import { compactNumber, ribbonPath, useSeriesColor } from '../../utils/chart';
|
|
92
92
|
import { cn } from '../../utils/cn';
|
|
93
|
+
import { useSkeletonHandoff } from '../../hooks/use-skeleton-handoff';
|
|
93
94
|
|
|
94
95
|
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
95
96
|
|
|
@@ -790,12 +791,19 @@ function FunnelChartSkeleton({ color }: FunnelChartSkeletonProps) {
|
|
|
790
791
|
const token = useCSSVariable('--color-skeleton');
|
|
791
792
|
const fill = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
|
|
792
793
|
|
|
793
|
-
|
|
794
|
+
// Held through the fade rather than to the frame the data lands, so the plain
|
|
795
|
+
// taper dissolves under the stages growing across it instead of leaving a
|
|
796
|
+
// blank panel between the two.
|
|
797
|
+
const { mounted, opacity } = useSkeletonHandoff(status === 'loading');
|
|
798
|
+
const animatedProps = useAnimatedProps(() => ({ opacity: opacity.value }));
|
|
799
|
+
|
|
800
|
+
if (!mounted || width <= 0 || height <= 0) return null;
|
|
794
801
|
|
|
795
802
|
const extent = Math.max(0, height / 2 - textBand);
|
|
796
803
|
|
|
797
804
|
return (
|
|
798
|
-
<
|
|
805
|
+
<AnimatedPath
|
|
806
|
+
animatedProps={animatedProps}
|
|
799
807
|
d={ribbonPath(0, width, extent, extent * 0.4, middle, curve)}
|
|
800
808
|
fill={fill}
|
|
801
809
|
/>
|
|
@@ -54,18 +54,18 @@ import Animated, {
|
|
|
54
54
|
Easing,
|
|
55
55
|
runOnJS,
|
|
56
56
|
useAnimatedProps,
|
|
57
|
+
useAnimatedStyle,
|
|
57
58
|
useReducedMotion,
|
|
58
59
|
useSharedValue,
|
|
59
60
|
withTiming,
|
|
60
61
|
} from 'react-native-reanimated';
|
|
61
|
-
import Svg, {
|
|
62
|
+
import Svg, { G, Line as SvgLine, Rect } from 'react-native-svg';
|
|
62
63
|
import { useCSSVariable } from 'uniwind';
|
|
63
64
|
import { Text } from '../../primitives/text';
|
|
64
65
|
import { ChartAccessibilityData, type ChartAccessibilityProps } from '../../primitives/chart-accessibility';
|
|
65
66
|
import { cn } from '../../utils/cn';
|
|
66
67
|
import { normalizeWeekStart, startOfDay } from '../../utils/date';
|
|
67
68
|
|
|
68
|
-
const AnimatedRect = Animated.createAnimatedComponent(Rect);
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Rows in a week — the default, and what the calendar helper and the weekday
|
|
@@ -187,7 +187,6 @@ interface HeatmapContextValue {
|
|
|
187
187
|
cornerRadius: number;
|
|
188
188
|
inactiveOpacity: number;
|
|
189
189
|
weekStartDay: number;
|
|
190
|
-
clipId: string;
|
|
191
190
|
activeCell: HeatmapCell | null;
|
|
192
191
|
setActiveCell: (cell: HeatmapCell | null) => void;
|
|
193
192
|
}
|
|
@@ -409,7 +408,6 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
|
|
|
409
408
|
) {
|
|
410
409
|
const [width, setWidth] = useState(0);
|
|
411
410
|
const [activeCell, setActiveCellState] = useState<HeatmapCell | null>(null);
|
|
412
|
-
const clipId = `panelui-heat-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
413
411
|
|
|
414
412
|
const reveal = useSharedValue(0);
|
|
415
413
|
const reducedMotion = useReducedMotion();
|
|
@@ -551,7 +549,6 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
|
|
|
551
549
|
cornerRadius,
|
|
552
550
|
inactiveOpacity,
|
|
553
551
|
weekStartDay,
|
|
554
|
-
clipId,
|
|
555
552
|
activeCell,
|
|
556
553
|
setActiveCell,
|
|
557
554
|
}),
|
|
@@ -565,7 +562,6 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
|
|
|
565
562
|
cornerRadius,
|
|
566
563
|
inactiveOpacity,
|
|
567
564
|
weekStartDay,
|
|
568
|
-
clipId,
|
|
569
565
|
activeCell,
|
|
570
566
|
setActiveCell,
|
|
571
567
|
]
|
|
@@ -576,7 +572,7 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
|
|
|
576
572
|
* effect is the same — columns arriving in order — and it costs one animated
|
|
577
573
|
* value instead of one per week, which for a year is fifty-two.
|
|
578
574
|
*/
|
|
579
|
-
const
|
|
575
|
+
const revealStyle = useAnimatedStyle(() => ({ width: grid.width * reveal.value }));
|
|
580
576
|
|
|
581
577
|
return (
|
|
582
578
|
<HeatmapContext.Provider value={context}>
|
|
@@ -625,23 +621,32 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
|
|
|
625
621
|
<View style={{ width: grid.width, height: grid.height }}>
|
|
626
622
|
{grid.width > 0 ? (
|
|
627
623
|
<>
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
624
|
+
{/*
|
|
625
|
+
* The reveal is a view that grows, not an SVG clip path.
|
|
626
|
+
*
|
|
627
|
+
* It used to be an animated `<Rect>` inside `<Defs>`, and on
|
|
628
|
+
* Android those animated props never reach the native clip, so
|
|
629
|
+
* the grid drew complete and the reveal did not play at all. A
|
|
630
|
+
* view with `overflow: 'hidden'` is clipped by the platform
|
|
631
|
+
* itself, which both platforms agree on.
|
|
632
|
+
*
|
|
633
|
+
* Animating `width` is normally a layout pass per frame. This
|
|
634
|
+
* view is absolutely positioned and its only child is an
|
|
635
|
+
* `<Svg>` with an explicit width and height, so it is one node
|
|
636
|
+
* and nothing around it moves.
|
|
637
|
+
*/}
|
|
638
|
+
<Animated.View
|
|
639
|
+
pointerEvents="none"
|
|
640
|
+
style={[
|
|
641
|
+
{ position: 'absolute', top: 0, bottom: 0, left: 0, overflow: 'hidden' },
|
|
642
|
+
revealStyle,
|
|
643
|
+
]}
|
|
644
|
+
>
|
|
645
|
+
<Svg width={grid.width} height={grid.height}>
|
|
641
646
|
{parts.rules}
|
|
642
647
|
{parts.cells}
|
|
643
|
-
</
|
|
644
|
-
</
|
|
648
|
+
</Svg>
|
|
649
|
+
</Animated.View>
|
|
645
650
|
{parts.tooltip}
|
|
646
651
|
</>
|
|
647
652
|
) : null}
|
|
@@ -526,21 +526,37 @@ const HexChartRoot = forwardRef<HexChartHandle, HexChartProps>(function HexChart
|
|
|
526
526
|
<>
|
|
527
527
|
<Svg width={width} height={height}>
|
|
528
528
|
<Defs>
|
|
529
|
+
{/*
|
|
530
|
+
* The reveal's own geometry is declared statically as well as
|
|
531
|
+
* animated, and it has to be. Animated props on an element
|
|
532
|
+
* inside `Defs` do not reach the native clip on every
|
|
533
|
+
* platform — Android is the one that does not — and with the
|
|
534
|
+
* size coming only from the animation there is no size at all
|
|
535
|
+
* where they do not: an empty clip, and a honeycomb whose
|
|
536
|
+
* series never appear while its field draws normally.
|
|
537
|
+
*
|
|
538
|
+
* Declared, the worst case is the reveal not playing and the
|
|
539
|
+
* split being there from the first frame, which is a chart
|
|
540
|
+
* that is merely less pleasing rather than a chart that is
|
|
541
|
+
* missing its answer.
|
|
542
|
+
*/}
|
|
529
543
|
<ClipPath id={clipId}>
|
|
530
544
|
{shape === 'blob' ? (
|
|
531
545
|
<AnimatedEllipse
|
|
532
546
|
cx={left + fieldWidth / 2}
|
|
533
547
|
cy={top + fieldHeight / 2}
|
|
548
|
+
rx={(fieldWidth / 2) * CORNER_REACH}
|
|
549
|
+
ry={(fieldHeight / 2) * CORNER_REACH}
|
|
534
550
|
animatedProps={ellipseProps}
|
|
535
551
|
/>
|
|
536
552
|
) : (
|
|
537
553
|
<AnimatedRect
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
554
|
+
x={0}
|
|
555
|
+
y={0}
|
|
556
|
+
width={width}
|
|
557
|
+
height={height}
|
|
558
|
+
animatedProps={rectProps}
|
|
559
|
+
/>
|
|
544
560
|
)}
|
|
545
561
|
</ClipPath>
|
|
546
562
|
</Defs>
|
|
@@ -71,13 +71,11 @@ import Animated, {
|
|
|
71
71
|
} from 'react-native-reanimated';
|
|
72
72
|
import Svg, {
|
|
73
73
|
Circle,
|
|
74
|
-
ClipPath,
|
|
75
74
|
Defs,
|
|
76
75
|
G,
|
|
77
76
|
Line as SvgLine,
|
|
78
77
|
LinearGradient,
|
|
79
78
|
Path,
|
|
80
|
-
Rect,
|
|
81
79
|
Stop,
|
|
82
80
|
} from 'react-native-svg';
|
|
83
81
|
import { useCSSVariable } from 'uniwind';
|
|
@@ -96,7 +94,6 @@ import {
|
|
|
96
94
|
import { cn } from '../../utils/cn';
|
|
97
95
|
|
|
98
96
|
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
99
|
-
const AnimatedRect = Animated.createAnimatedComponent(Rect);
|
|
100
97
|
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
101
98
|
const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
|
|
102
99
|
|
|
@@ -114,7 +111,7 @@ const Y_AXIS_GUTTER = 6;
|
|
|
114
111
|
* stays a flat list of children instead of two nested slots the caller has to
|
|
115
112
|
* remember the order of.
|
|
116
113
|
*/
|
|
117
|
-
type Layer = 'svg' | 'overlay' | 'header';
|
|
114
|
+
type Layer = 'svg' | 'series' | 'overlay' | 'header';
|
|
118
115
|
|
|
119
116
|
export type LineChartStatus = 'loading' | 'ready';
|
|
120
117
|
export type LineChartCurve = 'monotone' | 'linear';
|
|
@@ -143,7 +140,6 @@ interface LineChartContextValue {
|
|
|
143
140
|
activeIndex: SharedValue<number>;
|
|
144
141
|
activeIndexJS: number;
|
|
145
142
|
setActiveIndexJS: (index: number) => void;
|
|
146
|
-
clipId: string;
|
|
147
143
|
}
|
|
148
144
|
|
|
149
145
|
const LineChartContext = createContext<LineChartContextValue | null>(null);
|
|
@@ -242,7 +238,6 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
|
|
|
242
238
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
243
239
|
const [series, setSeries] = useState<[string, string][]>([]);
|
|
244
240
|
const [activeIndexJS, setActiveIndexJS] = useState(-1);
|
|
245
|
-
const clipId = `panelui-clip-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
246
241
|
|
|
247
242
|
const reveal = useSharedValue(0);
|
|
248
243
|
const domainMin = useSharedValue(0);
|
|
@@ -373,7 +368,7 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
|
|
|
373
368
|
// `replay()` re-runs the reveal — for a control the caller wires up.
|
|
374
369
|
useImperativeHandle(ref, () => ({ replay: playReveal }), [playReveal]);
|
|
375
370
|
|
|
376
|
-
const
|
|
371
|
+
const revealStyle = useAnimatedStyle(() => ({ width: plot.width * reveal.value }));
|
|
377
372
|
|
|
378
373
|
// One place the crosshair index lands, so the chart's own children and a
|
|
379
374
|
// readout outside it never disagree about which point is active.
|
|
@@ -411,7 +406,6 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
|
|
|
411
406
|
activeIndex,
|
|
412
407
|
activeIndexJS,
|
|
413
408
|
setActiveIndexJS: handleActiveIndex,
|
|
414
|
-
clipId,
|
|
415
409
|
}),
|
|
416
410
|
// `plot` is rebuilt every render from `size`, so it is compared by value.
|
|
417
411
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -433,11 +427,10 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
|
|
|
433
427
|
activeIndex,
|
|
434
428
|
activeIndexJS,
|
|
435
429
|
handleActiveIndex,
|
|
436
|
-
clipId,
|
|
437
430
|
]
|
|
438
431
|
);
|
|
439
432
|
|
|
440
|
-
const { svg, overlay, header } = partition(children);
|
|
433
|
+
const { svg, series: seriesLayer, overlay, header } = partition(children);
|
|
441
434
|
|
|
442
435
|
/*
|
|
443
436
|
* Two views, because the header is not part of the plot. `aspectRatio` and
|
|
@@ -472,32 +465,42 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
|
|
|
472
465
|
{plot.width > 0 ? (
|
|
473
466
|
<>
|
|
474
467
|
<Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
|
|
475
|
-
<Defs>
|
|
476
|
-
{/*
|
|
477
|
-
* One clip for everything in the plot. Sharing it is what
|
|
478
|
-
* makes the reveal read as the chart arriving, rather than as
|
|
479
|
-
* three separate things animating in at once.
|
|
480
|
-
*
|
|
481
|
-
* `width` is set statically as well as animated, and it has
|
|
482
|
-
* to be. Animated props on an element inside `Defs` do not
|
|
483
|
-
* reach the native clip on every platform, and with the width
|
|
484
|
-
* coming only from the animation there is no width at all
|
|
485
|
-
* when they do not — an empty clip, and a chart whose marks
|
|
486
|
-
* are all invisible while its axes draw normally. Declared,
|
|
487
|
-
* the worst case is the reveal not playing.
|
|
488
|
-
*/}
|
|
489
|
-
<ClipPath id={clipId}>
|
|
490
|
-
<AnimatedRect
|
|
491
|
-
x={plot.left}
|
|
492
|
-
y={0}
|
|
493
|
-
width={plot.width}
|
|
494
|
-
height={size.height}
|
|
495
|
-
animatedProps={clipProps}
|
|
496
|
-
/>
|
|
497
|
-
</ClipPath>
|
|
498
|
-
</Defs>
|
|
499
468
|
{svg}
|
|
500
469
|
</Svg>
|
|
470
|
+
{/*
|
|
471
|
+
* The reveal is a view that grows, not an SVG clip path.
|
|
472
|
+
*
|
|
473
|
+
* It used to be an animated `<Rect>` inside `<Defs>`, and on
|
|
474
|
+
* Android those animated props never reach the native clip — the
|
|
475
|
+
* rect keeps whatever width was declared on it, so the chart drew
|
|
476
|
+
* complete and the reveal simply did not play. A view with
|
|
477
|
+
* `overflow: 'hidden'` is clipped by the platform itself, which
|
|
478
|
+
* both platforms agree on.
|
|
479
|
+
*
|
|
480
|
+
* Animating `width` is normally a layout pass per frame. Here the
|
|
481
|
+
* view is absolutely positioned and its only child is an `<Svg>`
|
|
482
|
+
* with an explicit width and height, so the work is one node and
|
|
483
|
+
* nothing around it moves.
|
|
484
|
+
*
|
|
485
|
+
* The inner `<Svg>` is pulled back by `plot.left` so the series
|
|
486
|
+
* keeps the same coordinate space as the grid underneath it —
|
|
487
|
+
* otherwise every mark would shift by the y-axis gutter.
|
|
488
|
+
*/}
|
|
489
|
+
<Animated.View
|
|
490
|
+
pointerEvents="none"
|
|
491
|
+
style={[
|
|
492
|
+
{ position: 'absolute', top: 0, bottom: 0, left: plot.left, overflow: 'hidden' },
|
|
493
|
+
revealStyle,
|
|
494
|
+
]}
|
|
495
|
+
>
|
|
496
|
+
<Svg
|
|
497
|
+
width={size.width}
|
|
498
|
+
height={size.height}
|
|
499
|
+
style={{ position: 'absolute', top: 0, left: -plot.left }}
|
|
500
|
+
>
|
|
501
|
+
{seriesLayer}
|
|
502
|
+
</Svg>
|
|
503
|
+
</Animated.View>
|
|
501
504
|
{overlay}
|
|
502
505
|
</>
|
|
503
506
|
) : null}
|
|
@@ -511,20 +514,29 @@ LineChartRoot.displayName = 'LineChart';
|
|
|
511
514
|
/** Sorts the children into the SVG tree and the view layer over it. */
|
|
512
515
|
function partition(children: ReactNode) {
|
|
513
516
|
const svg: ReactNode[] = [];
|
|
517
|
+
const series: ReactNode[] = [];
|
|
514
518
|
const overlay: ReactNode[] = [];
|
|
515
519
|
const header: ReactNode[] = [];
|
|
516
520
|
|
|
517
521
|
Children.forEach(children, (child, index) => {
|
|
518
522
|
if (!isValidElement(child)) return;
|
|
519
523
|
const layer = (child.type as { layer?: Layer }).layer ?? 'svg';
|
|
520
|
-
|
|
524
|
+
const bucket =
|
|
525
|
+
layer === 'header'
|
|
526
|
+
? header
|
|
527
|
+
: layer === 'overlay'
|
|
528
|
+
? overlay
|
|
529
|
+
: layer === 'series'
|
|
530
|
+
? series
|
|
531
|
+
: svg;
|
|
532
|
+
bucket.push(
|
|
521
533
|
// Children of a `Children.forEach` need keys of their own once they are
|
|
522
534
|
// put into a new array.
|
|
523
535
|
<ChildSlot key={index}>{child}</ChildSlot>
|
|
524
536
|
);
|
|
525
537
|
});
|
|
526
538
|
|
|
527
|
-
return { svg, overlay, header };
|
|
539
|
+
return { svg, series, overlay, header };
|
|
528
540
|
}
|
|
529
541
|
|
|
530
542
|
/** Identity wrapper, purely so the partitioned arrays can carry keys. */
|
|
@@ -600,7 +612,7 @@ function LineChartLine({
|
|
|
600
612
|
dashArray,
|
|
601
613
|
showMarkers = false,
|
|
602
614
|
}: LineChartLineProps) {
|
|
603
|
-
const { data, plot, domainMin, domainMax, curve, status, registerSeries, unregisterSeries
|
|
615
|
+
const { data, plot, domainMin, domainMax, curve, status, registerSeries, unregisterSeries } =
|
|
604
616
|
useChart('LineChart.Line');
|
|
605
617
|
const stroke = useSeriesColor(color, colorIndex);
|
|
606
618
|
|
|
@@ -617,7 +629,7 @@ function LineChartLine({
|
|
|
617
629
|
}));
|
|
618
630
|
|
|
619
631
|
return (
|
|
620
|
-
<G
|
|
632
|
+
<G>
|
|
621
633
|
<AnimatedPath
|
|
622
634
|
animatedProps={animatedProps}
|
|
623
635
|
fill="none"
|
|
@@ -647,7 +659,7 @@ function LineChartLine({
|
|
|
647
659
|
);
|
|
648
660
|
}
|
|
649
661
|
LineChartLine.displayName = 'LineChart.Line';
|
|
650
|
-
LineChartLine.layer = '
|
|
662
|
+
LineChartLine.layer = 'series' as Layer;
|
|
651
663
|
|
|
652
664
|
/** A dot at one point. Follows the y-domain tween exactly as the line does. */
|
|
653
665
|
function PointMarker({
|
|
@@ -690,7 +702,7 @@ export interface LineChartAreaProps {
|
|
|
690
702
|
* fills over each other make a third colour that means nothing.
|
|
691
703
|
*/
|
|
692
704
|
function LineChartArea({ dataKey, color, colorIndex = 1, opacity = 0.18 }: LineChartAreaProps) {
|
|
693
|
-
const { data, plot, domainMin, domainMax, curve, status
|
|
705
|
+
const { data, plot, domainMin, domainMax, curve, status } = useChart('LineChart.Area');
|
|
694
706
|
const fill = useSeriesColor(color, colorIndex);
|
|
695
707
|
const gradientId = `panelui-area-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
696
708
|
|
|
@@ -702,7 +714,7 @@ function LineChartArea({ dataKey, color, colorIndex = 1, opacity = 0.18 }: LineC
|
|
|
702
714
|
}));
|
|
703
715
|
|
|
704
716
|
return (
|
|
705
|
-
<G
|
|
717
|
+
<G>
|
|
706
718
|
<Defs>
|
|
707
719
|
<LinearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
|
708
720
|
<Stop offset="0" stopColor={fill} stopOpacity={opacity} />
|
|
@@ -714,7 +726,7 @@ function LineChartArea({ dataKey, color, colorIndex = 1, opacity = 0.18 }: LineC
|
|
|
714
726
|
);
|
|
715
727
|
}
|
|
716
728
|
LineChartArea.displayName = 'LineChart.Area';
|
|
717
|
-
LineChartArea.layer = '
|
|
729
|
+
LineChartArea.layer = 'series' as Layer;
|
|
718
730
|
|
|
719
731
|
export interface LineChartSkeletonProps {
|
|
720
732
|
/** Milliseconds for one pass of the sweep. */
|
|
@@ -733,6 +745,9 @@ function LineChartSkeleton({ duration = 1400, color }: LineChartSkeletonProps) {
|
|
|
733
745
|
const token = useCSSVariable('--color-skeleton');
|
|
734
746
|
const base = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
|
|
735
747
|
const highlight = useSeriesColor(undefined, 1);
|
|
748
|
+
// Unique per instance: a fixed id makes two of these on one screen share a
|
|
749
|
+
// gradient, and the second one to mount wins.
|
|
750
|
+
const gradientId = `panelui-chart-skeleton-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
736
751
|
|
|
737
752
|
const sweep = useSharedValue(0);
|
|
738
753
|
const reducedMotion = useReducedMotion();
|
|
@@ -759,7 +774,6 @@ function LineChartSkeleton({ duration = 1400, color }: LineChartSkeletonProps) {
|
|
|
759
774
|
if (!loading) return null;
|
|
760
775
|
|
|
761
776
|
const y = plot.top + plot.height / 2;
|
|
762
|
-
const gradientId = 'panelui-chart-skeleton';
|
|
763
777
|
|
|
764
778
|
return (
|
|
765
779
|
<G>
|