panelui-native 0.59.0 → 0.61.1
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 +3 -1
- package/lib/module/components/area-chart/index.js +86 -1
- package/lib/module/components/area-chart/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +112 -2
- package/lib/module/components/bar-chart/index.js.map +1 -1
- package/lib/module/components/fab/index.js +63 -30
- package/lib/module/components/fab/index.js.map +1 -1
- package/lib/module/components/selection-mode/index.js +805 -0
- package/lib/module/components/selection-mode/index.js.map +1 -0
- package/lib/module/components/tabs/index.js +384 -341
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/treemap-chart/index.js +957 -0
- package/lib/module/components/treemap-chart/index.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/chart.js +131 -4
- package/lib/module/utils/chart.js.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts +28 -1
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts +33 -3
- package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/fab/index.d.ts +11 -5
- package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
- package/lib/typescript/src/components/selection-mode/index.d.ts +498 -0
- package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -0
- package/lib/typescript/src/components/tabs/index.d.ts +64 -46
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/treemap-chart/index.d.ts +317 -0
- package/lib/typescript/src/components/treemap-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +26 -1
- package/lib/typescript/src/utils/chart.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/area-chart/index.tsx +86 -1
- package/src/components/bar-chart/index.tsx +115 -4
- package/src/components/fab/index.tsx +66 -31
- package/src/components/selection-mode/index.tsx +1027 -0
- package/src/components/tabs/index.tsx +452 -413
- package/src/components/treemap-chart/index.tsx +1175 -0
- package/src/index.ts +30 -0
- package/src/utils/chart.ts +158 -4
|
@@ -52,15 +52,17 @@ import { StyleSheet, View, type LayoutChangeEvent, type ViewProps } from 'react-
|
|
|
52
52
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
53
53
|
import Animated, {
|
|
54
54
|
Easing,
|
|
55
|
+
cancelAnimation,
|
|
55
56
|
runOnJS,
|
|
56
57
|
useAnimatedProps,
|
|
57
58
|
useAnimatedStyle,
|
|
58
59
|
useReducedMotion,
|
|
59
60
|
useSharedValue,
|
|
61
|
+
withRepeat,
|
|
60
62
|
withTiming,
|
|
61
63
|
type SharedValue,
|
|
62
64
|
} from 'react-native-reanimated';
|
|
63
|
-
import Svg, { G, Line as SvgLine, Path } from 'react-native-svg';
|
|
65
|
+
import Svg, { Defs, G, Line as SvgLine, LinearGradient, Path, Stop } from 'react-native-svg';
|
|
64
66
|
import { useCSSVariable } from 'uniwind';
|
|
65
67
|
import { Text } from '../../primitives/text';
|
|
66
68
|
import {
|
|
@@ -75,6 +77,7 @@ import {
|
|
|
75
77
|
import { cn } from '../../utils/cn';
|
|
76
78
|
|
|
77
79
|
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
80
|
+
const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
|
|
78
81
|
|
|
79
82
|
/** Room left around the plot for the axis labels. */
|
|
80
83
|
const PADDING = { top: 12, right: 10, bottom: 22, left: 10 };
|
|
@@ -163,9 +166,10 @@ export interface BarChartProps extends ViewProps {
|
|
|
163
166
|
/** Key holding the category label. Used by the axis and the readout. */
|
|
164
167
|
xDataKey?: string;
|
|
165
168
|
/**
|
|
166
|
-
* `loading`
|
|
167
|
-
*
|
|
168
|
-
* swapped for a chart — swapping loses the transition.
|
|
169
|
+
* `loading` holds the bars at the baseline and grows them into the real ones
|
|
170
|
+
* when it turns `ready`. One component throughout, rather than a spinner
|
|
171
|
+
* swapped for a chart — swapping loses the transition. Add a
|
|
172
|
+
* `BarChart.Skeleton` for something to stand in the plot meanwhile.
|
|
169
173
|
*/
|
|
170
174
|
status?: BarChartStatus;
|
|
171
175
|
/** Width ÷ height. `2` is the wide card shape. */
|
|
@@ -738,6 +742,112 @@ function BarChartBar({ dataKey, color, colorIndex = 1, cornerRadius }: BarChartB
|
|
|
738
742
|
BarChartBar.displayName = 'BarChart.Bar';
|
|
739
743
|
BarChartBar.layer = 'svg' as Layer;
|
|
740
744
|
|
|
745
|
+
/** How much of the value axis a placeholder bar takes. */
|
|
746
|
+
const SKELETON_LENGTH = 0.22;
|
|
747
|
+
|
|
748
|
+
/** Bands to draw when there is no data yet to count them from. */
|
|
749
|
+
const SKELETON_BARS = 7;
|
|
750
|
+
|
|
751
|
+
export interface BarChartSkeletonProps {
|
|
752
|
+
/**
|
|
753
|
+
* How many placeholder bars to draw. Defaults to one per row, and to seven
|
|
754
|
+
* when the data has not arrived — the count is the one thing a loading
|
|
755
|
+
* chart can be honest about only if it already has the rows.
|
|
756
|
+
*/
|
|
757
|
+
bars?: number;
|
|
758
|
+
/** Milliseconds for one pass of the sweep. */
|
|
759
|
+
duration?: number;
|
|
760
|
+
color?: string;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* The loading state: a row of short, equal stubs on the baseline, with a
|
|
765
|
+
* highlight travelling across them.
|
|
766
|
+
*
|
|
767
|
+
* Equal on purpose. Placeholder bars of differing heights are a distribution
|
|
768
|
+
* the reader has no way to tell from the real one until it changes under them,
|
|
769
|
+
* so these say only how many bars there will be and where the baseline is.
|
|
770
|
+
*
|
|
771
|
+
* The sweep is the part that carries the meaning. Without it a chart waiting
|
|
772
|
+
* for data and a chart whose values are all zero draw the same picture, and
|
|
773
|
+
* the reader is left to guess which one they are looking at.
|
|
774
|
+
*/
|
|
775
|
+
function BarChartSkeleton({ bars, duration = 1400, color }: BarChartSkeletonProps) {
|
|
776
|
+
const { plot, status, orientation, data, barGap, barWidth, cornerRadius } =
|
|
777
|
+
useChart('BarChart.Skeleton');
|
|
778
|
+
const token = useCSSVariable('--color-skeleton');
|
|
779
|
+
const base = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
|
|
780
|
+
const highlight = useSeriesColor(undefined, 1);
|
|
781
|
+
|
|
782
|
+
const sweep = useSharedValue(0);
|
|
783
|
+
const reducedMotion = useReducedMotion();
|
|
784
|
+
const loading = status === 'loading';
|
|
785
|
+
|
|
786
|
+
useEffect(() => {
|
|
787
|
+
if (!loading || reducedMotion) {
|
|
788
|
+
cancelAnimation(sweep);
|
|
789
|
+
sweep.value = 0;
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
sweep.value = 0;
|
|
793
|
+
sweep.value = withRepeat(withTiming(1, { duration, easing: Easing.linear }), -1, false);
|
|
794
|
+
return () => cancelAnimation(sweep);
|
|
795
|
+
}, [loading, reducedMotion, duration, sweep]);
|
|
796
|
+
|
|
797
|
+
// The band travels by moving the gradient's own endpoints, so the whole
|
|
798
|
+
// effect is two numbers changing on the UI thread.
|
|
799
|
+
const animatedProps = useAnimatedProps(() => ({
|
|
800
|
+
x1: `${(sweep.value * 1.4 - 0.4) * 100}%`,
|
|
801
|
+
x2: `${(sweep.value * 1.4 - 0.4 + 0.4) * 100}%`,
|
|
802
|
+
}));
|
|
803
|
+
|
|
804
|
+
const horizontal = orientation === 'horizontal';
|
|
805
|
+
const total = Math.max(1, bars ?? (data.length || SKELETON_BARS));
|
|
806
|
+
|
|
807
|
+
const d = useMemo(() => {
|
|
808
|
+
if (plot.width <= 0 || plot.height <= 0) return '';
|
|
809
|
+
|
|
810
|
+
// The same two axes the bars use: `along` is the category one, `across`
|
|
811
|
+
// the value one, so a sideways chart is this code with the pair swapped.
|
|
812
|
+
const along = horizontal ? plot.height : plot.width;
|
|
813
|
+
const alongStart = horizontal ? plot.top : plot.left;
|
|
814
|
+
const across = horizontal ? plot.width : plot.height;
|
|
815
|
+
|
|
816
|
+
const band = along / total;
|
|
817
|
+
const usable = band * (1 - barGap);
|
|
818
|
+
const thickness = Math.min(barWidth ?? usable, usable);
|
|
819
|
+
const length = across * SKELETON_LENGTH;
|
|
820
|
+
|
|
821
|
+
let path = '';
|
|
822
|
+
for (let i = 0; i < total; i += 1) {
|
|
823
|
+
const lead = alongStart + i * band + (band - thickness) / 2;
|
|
824
|
+
path += horizontal
|
|
825
|
+
? barPath(plot.left, lead, length, thickness, cornerRadius, 'right')
|
|
826
|
+
: barPath(lead, plot.top + plot.height - length, thickness, length, cornerRadius, 'up');
|
|
827
|
+
}
|
|
828
|
+
return path;
|
|
829
|
+
}, [plot, horizontal, total, barGap, barWidth, cornerRadius]);
|
|
830
|
+
|
|
831
|
+
if (!loading || !d) return null;
|
|
832
|
+
|
|
833
|
+
const gradientId = 'panelui-bar-skeleton';
|
|
834
|
+
|
|
835
|
+
return (
|
|
836
|
+
<G>
|
|
837
|
+
<Defs>
|
|
838
|
+
<AnimatedLinearGradient id={gradientId} animatedProps={animatedProps} y1="0" y2="0">
|
|
839
|
+
<Stop offset="0" stopColor={base} />
|
|
840
|
+
<Stop offset="0.5" stopColor={highlight} stopOpacity={0.55} />
|
|
841
|
+
<Stop offset="1" stopColor={base} />
|
|
842
|
+
</AnimatedLinearGradient>
|
|
843
|
+
</Defs>
|
|
844
|
+
<Path d={d} fill={`url(#${gradientId})`} />
|
|
845
|
+
</G>
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
BarChartSkeleton.displayName = 'BarChart.Skeleton';
|
|
849
|
+
BarChartSkeleton.layer = 'svg' as Layer;
|
|
850
|
+
|
|
741
851
|
/* -------------------------------------------------------------------------- */
|
|
742
852
|
/* Overlay layer */
|
|
743
853
|
/* -------------------------------------------------------------------------- */
|
|
@@ -1201,6 +1311,7 @@ export const BarChart = Object.assign(BarChartRoot, {
|
|
|
1201
1311
|
Header: BarChartHeader,
|
|
1202
1312
|
Grid: BarChartGrid,
|
|
1203
1313
|
Bar: BarChartBar,
|
|
1314
|
+
Skeleton: BarChartSkeleton,
|
|
1204
1315
|
XAxis: BarChartXAxis,
|
|
1205
1316
|
YAxis: BarChartYAxis,
|
|
1206
1317
|
Tooltip: BarChartTooltip,
|
|
@@ -48,14 +48,21 @@
|
|
|
48
48
|
* modal — the next tap either picks something or closes it — and a scrim is what
|
|
49
49
|
* says so, as well as what catches the tap that closes it.
|
|
50
50
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
51
|
+
* ## Where a group has to be written
|
|
52
|
+
*
|
|
53
|
+
* A `Fab.Group` draws its scrim and its buttons as two absolutely positioned
|
|
54
|
+
* siblings in whatever it is written inside, so put it in the screen's root
|
|
55
|
+
* container. That is where its `offset` is measured from, and it is what the
|
|
56
|
+
* scrim covers.
|
|
57
|
+
*
|
|
58
|
+
* It stays in the screen's own view tree rather than being lifted out of it,
|
|
59
|
+
* which is the part that matters after navigation: pushing a screen over this
|
|
60
|
+
* one hides the dial with everything else on it. A group lifted to the top of
|
|
61
|
+
* the app would still be drawn over the screen that replaced it.
|
|
56
62
|
*/
|
|
57
63
|
import {
|
|
58
64
|
Children,
|
|
65
|
+
cloneElement,
|
|
59
66
|
createContext,
|
|
60
67
|
forwardRef,
|
|
61
68
|
isValidElement,
|
|
@@ -63,6 +70,7 @@ import {
|
|
|
63
70
|
useContext,
|
|
64
71
|
useMemo,
|
|
65
72
|
useState,
|
|
73
|
+
type ReactElement,
|
|
66
74
|
type ReactNode,
|
|
67
75
|
} from 'react';
|
|
68
76
|
import { Pressable, StyleSheet, View, type ViewProps } from 'react-native';
|
|
@@ -76,8 +84,8 @@ import Animated, {
|
|
|
76
84
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
77
85
|
import { useCSSVariable } from 'uniwind';
|
|
78
86
|
import { IconColorProvider } from '../../icons';
|
|
87
|
+
import { useBackHandler } from '../../hooks/use-back-handler';
|
|
79
88
|
import { AnimatedPressable } from '../../primitives/animated-pressable';
|
|
80
|
-
import { Portal } from '../../primitives/portal';
|
|
81
89
|
import { Scrim } from '../../primitives/scrim';
|
|
82
90
|
import { Text } from '../../primitives/text';
|
|
83
91
|
import { cn } from '../../utils/cn';
|
|
@@ -271,6 +279,9 @@ FabRoot.displayName = 'Fab';
|
|
|
271
279
|
* Group — the speed dial
|
|
272
280
|
* -------------------------------------------------------------------------- */
|
|
273
281
|
|
|
282
|
+
/** Whatever was written as a dial child, and the one prop a slot reaches for. */
|
|
283
|
+
type PressableChild = ReactElement<{ onPress?: () => void }>;
|
|
284
|
+
|
|
274
285
|
interface FabGroupContextValue {
|
|
275
286
|
/** 0 closed, 1 open. Every action reads it and its own index off it. */
|
|
276
287
|
progress: SharedValue<number>;
|
|
@@ -324,10 +335,11 @@ export interface FabGroupProps extends Omit<ViewProps, 'children'> {
|
|
|
324
335
|
/**
|
|
325
336
|
* A trigger with actions behind it.
|
|
326
337
|
*
|
|
327
|
-
* The scrim
|
|
328
|
-
* group's own
|
|
329
|
-
*
|
|
330
|
-
*
|
|
338
|
+
* The scrim and the dial are two absolutely positioned siblings written into
|
|
339
|
+
* the group's own parent — the scrim first, so the dial is drawn over it. Both
|
|
340
|
+
* are laid out against that parent, which is why a group belongs in the
|
|
341
|
+
* screen's root container: it is what `offset` is measured from and what the
|
|
342
|
+
* scrim covers.
|
|
331
343
|
*/
|
|
332
344
|
const FabGroup = forwardRef<View, FabGroupProps>(
|
|
333
345
|
(
|
|
@@ -363,7 +375,7 @@ const FabGroup = forwardRef<View, FabGroupProps>(
|
|
|
363
375
|
[openProp, onOpenChange]
|
|
364
376
|
);
|
|
365
377
|
|
|
366
|
-
const actions = Children.toArray(children).filter(isValidElement);
|
|
378
|
+
const actions = Children.toArray(children).filter(isValidElement) as PressableChild[];
|
|
367
379
|
/*
|
|
368
380
|
* The count, pulled out as a plain number before any worklet sees it.
|
|
369
381
|
*
|
|
@@ -406,28 +418,32 @@ const FabGroup = forwardRef<View, FabGroupProps>(
|
|
|
406
418
|
setOpen(!open);
|
|
407
419
|
}, [haptics, open, setOpen]);
|
|
408
420
|
|
|
421
|
+
// An open dial owns the back button: back should shut it, not leave it
|
|
422
|
+
// standing over the screen underneath.
|
|
423
|
+
useBackHandler(open, close);
|
|
424
|
+
|
|
409
425
|
/*
|
|
410
|
-
*
|
|
426
|
+
* Two absolutely positioned siblings, scrim first, both in the group's own
|
|
427
|
+
* parent — which is why a group belongs in the screen's root container.
|
|
411
428
|
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
429
|
+
* This used to go through a portal, and the portal was the bug. Portalled
|
|
430
|
+
* content is mounted at the app root, above the router, and is removed only
|
|
431
|
+
* when the component that declared it unmounts. A stack keeps the screen
|
|
432
|
+
* you pushed from mounted, so a group declared on that screen carried on
|
|
433
|
+
* drawing over every screen after it — open or closed, since the trigger
|
|
434
|
+
* travelled through the portal too.
|
|
418
435
|
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
436
|
+
* Ordering is the reason the portal was reached for in the first place: a
|
|
437
|
+
* scrim that covers the screen must not cover the dial it belongs to. As
|
|
438
|
+
* siblings it comes out right for free — the scrim is written first and the
|
|
439
|
+
* dial after it, and the later sibling draws on top.
|
|
423
440
|
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
* the content it belongs to instead of at the root of the screen.
|
|
441
|
+
* Nothing remounts when the dial opens. The trigger is in the same place in
|
|
442
|
+
* the tree either way, so opening adds the scrim and the actions and leaves
|
|
443
|
+
* the button alone.
|
|
428
444
|
*/
|
|
429
445
|
return (
|
|
430
|
-
|
|
446
|
+
<>
|
|
431
447
|
{open ? (
|
|
432
448
|
// Catches the tap that closes the dial, and says it is modal.
|
|
433
449
|
<Pressable
|
|
@@ -478,7 +494,7 @@ const FabGroup = forwardRef<View, FabGroupProps>(
|
|
|
478
494
|
</FabRoot>
|
|
479
495
|
</View>
|
|
480
496
|
</FabGroupContext.Provider>
|
|
481
|
-
|
|
497
|
+
</>
|
|
482
498
|
);
|
|
483
499
|
}
|
|
484
500
|
);
|
|
@@ -491,8 +507,27 @@ FabGroup.displayName = 'Fab.Group';
|
|
|
491
507
|
* The stagger runs bottom-up: the action nearest the trigger arrives first,
|
|
492
508
|
* which is the order a hand travelling away from the button meets them in.
|
|
493
509
|
*/
|
|
494
|
-
function FabActionSlot({ index, children }: { index: number; children:
|
|
495
|
-
const { progress, count } = useFabGroup('Fab.Action');
|
|
510
|
+
function FabActionSlot({ index, children }: { index: number; children: PressableChild }) {
|
|
511
|
+
const { progress, count, close } = useFabGroup('Fab.Action');
|
|
512
|
+
|
|
513
|
+
/*
|
|
514
|
+
* Whatever is in the slot closes the dial when it is pressed.
|
|
515
|
+
*
|
|
516
|
+
* `Fab.Action` does this itself, but a plain `Fab` written as a child is a
|
|
517
|
+
* perfectly reasonable thing to reach for and knows nothing about the dial it
|
|
518
|
+
* is in. Left alone it runs its action — navigating, usually — with the dial
|
|
519
|
+
* still open behind it. Wrapping the handler here covers both, and closing
|
|
520
|
+
* twice is closing once.
|
|
521
|
+
*/
|
|
522
|
+
const { onPress } = children.props;
|
|
523
|
+
const child = onPress
|
|
524
|
+
? cloneElement(children, {
|
|
525
|
+
onPress: () => {
|
|
526
|
+
close();
|
|
527
|
+
onPress();
|
|
528
|
+
},
|
|
529
|
+
})
|
|
530
|
+
: children;
|
|
496
531
|
|
|
497
532
|
const style = useAnimatedStyle(() => {
|
|
498
533
|
const steps = Math.max(1, count);
|
|
@@ -505,7 +540,7 @@ function FabActionSlot({ index, children }: { index: number; children: ReactNode
|
|
|
505
540
|
};
|
|
506
541
|
});
|
|
507
542
|
|
|
508
|
-
return <Animated.View style={style}>{
|
|
543
|
+
return <Animated.View style={style}>{child}</Animated.View>;
|
|
509
544
|
}
|
|
510
545
|
|
|
511
546
|
/* -------------------------------------------------------------------------- *
|