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
|
@@ -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>
|
|
@@ -44,7 +44,15 @@
|
|
|
44
44
|
* Screen readers get one copy. The rest are duplicates of content already
|
|
45
45
|
* announced, and hearing a sponsor list four times over is not thoroughness.
|
|
46
46
|
*/
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
createContext,
|
|
49
|
+
useContext,
|
|
50
|
+
useEffect,
|
|
51
|
+
useMemo,
|
|
52
|
+
useState,
|
|
53
|
+
type ComponentType,
|
|
54
|
+
type ReactNode,
|
|
55
|
+
} from 'react';
|
|
48
56
|
import {
|
|
49
57
|
Platform,
|
|
50
58
|
Pressable,
|
|
@@ -99,7 +107,13 @@ export interface MarqueeProps extends Omit<ViewProps, 'children'> {
|
|
|
99
107
|
reverse?: boolean;
|
|
100
108
|
/** Set false to hold the content where it is. */
|
|
101
109
|
playing?: boolean;
|
|
102
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Show the built-in user pause/play control while motion is enabled.
|
|
112
|
+
*
|
|
113
|
+
* Defaults to `true` on its own, and to `false` inside a `Marquee.Group` —
|
|
114
|
+
* the group draws one control for everything in it, and a control per row is
|
|
115
|
+
* how two of them end up stacked on top of each other.
|
|
116
|
+
*/
|
|
103
117
|
showPauseControl?: boolean;
|
|
104
118
|
/** Visible and spoken label for the moving state. */
|
|
105
119
|
pauseLabel?: string;
|
|
@@ -109,7 +123,18 @@ export interface MarqueeProps extends Omit<ViewProps, 'children'> {
|
|
|
109
123
|
onPlayingChange?: (playing: boolean) => void;
|
|
110
124
|
}
|
|
111
125
|
|
|
112
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Set by `Marquee.Group` so the marquees inside it stop drawing a pause control
|
|
128
|
+
* each and take the group's instead.
|
|
129
|
+
*/
|
|
130
|
+
interface MarqueeGroupContextValue {
|
|
131
|
+
/** True while the group's control is holding everything in it still. */
|
|
132
|
+
paused: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const MarqueeGroupContext = createContext<MarqueeGroupContextValue | null>(null);
|
|
136
|
+
|
|
137
|
+
function MarqueeRoot({
|
|
113
138
|
className,
|
|
114
139
|
children,
|
|
115
140
|
speed: speedProp = DEFAULT_MARQUEE_SPEED,
|
|
@@ -117,7 +142,7 @@ export function Marquee({
|
|
|
117
142
|
direction = 'horizontal',
|
|
118
143
|
reverse = false,
|
|
119
144
|
playing = true,
|
|
120
|
-
showPauseControl
|
|
145
|
+
showPauseControl,
|
|
121
146
|
pauseLabel = 'Pause',
|
|
122
147
|
playLabel = 'Play',
|
|
123
148
|
onPlayingChange,
|
|
@@ -135,7 +160,13 @@ export function Marquee({
|
|
|
135
160
|
const [content, setContent] = useState(0);
|
|
136
161
|
const [userPaused, setUserPaused] = useState(false);
|
|
137
162
|
const speed = normalizeMarqueeSpeed(speedProp);
|
|
138
|
-
|
|
163
|
+
|
|
164
|
+
// A group's pause is the same instruction as this one's, so it is read here
|
|
165
|
+
// rather than pushed down as a `playing` prop — a caller who set `playing`
|
|
166
|
+
// themselves should not have it overwritten by the container.
|
|
167
|
+
const group = useContext(MarqueeGroupContext);
|
|
168
|
+
const showControl = showPauseControl ?? group === null;
|
|
169
|
+
const moving = playing && !userPaused && !(group?.paused ?? false);
|
|
139
170
|
|
|
140
171
|
// The distance from one copy to the same point on the next, and therefore
|
|
141
172
|
// both the layout step and the exact loop length. They are the same number
|
|
@@ -250,7 +281,7 @@ export function Marquee({
|
|
|
250
281
|
);
|
|
251
282
|
})}
|
|
252
283
|
</Animated.View>
|
|
253
|
-
{
|
|
284
|
+
{showControl && playing && period > 0 && speed > 0 ? (
|
|
254
285
|
<Pressable
|
|
255
286
|
accessibilityRole="button"
|
|
256
287
|
accessibilityLabel={userPaused ? playLabel : pauseLabel}
|
|
@@ -270,6 +301,98 @@ export function Marquee({
|
|
|
270
301
|
);
|
|
271
302
|
}
|
|
272
303
|
|
|
304
|
+
export interface MarqueeGroupProps extends ViewProps {
|
|
305
|
+
className?: string;
|
|
306
|
+
/** Set false to hold every marquee in the group where it is. */
|
|
307
|
+
playing?: boolean;
|
|
308
|
+
/** Show the group's pause/play control. */
|
|
309
|
+
showPauseControl?: boolean;
|
|
310
|
+
/** Visible and spoken label for the moving state. */
|
|
311
|
+
pauseLabel?: string;
|
|
312
|
+
/** Visible and spoken label for the user-paused state. */
|
|
313
|
+
playLabel?: string;
|
|
314
|
+
/** Reports changes made by the group's control. */
|
|
315
|
+
onPlayingChange?: (playing: boolean) => void;
|
|
316
|
+
children?: ReactNode;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Rows of travelling content that stop and start together, under one control.
|
|
321
|
+
*
|
|
322
|
+
* Moving content needs a way to stop it, so a marquee draws its own pause
|
|
323
|
+
* button. Stacked — two rows of logos travelling against each other — that is
|
|
324
|
+
* one button per row, each pinned to its own bottom corner, and the upper one
|
|
325
|
+
* lands on top of the row beneath it. Two buttons for one piece of motion, and
|
|
326
|
+
* one of them covering the content it belongs to.
|
|
327
|
+
*
|
|
328
|
+
* A group is the answer to both: the marquees inside it stop drawing their own
|
|
329
|
+
* control, and the group draws a single one below them rather than over them.
|
|
330
|
+
*
|
|
331
|
+
* ```tsx
|
|
332
|
+
* <Marquee.Group>
|
|
333
|
+
* <Marquee>{topRow}</Marquee>
|
|
334
|
+
* <Marquee reverse>{bottomRow}</Marquee>
|
|
335
|
+
* </Marquee.Group>
|
|
336
|
+
* ```
|
|
337
|
+
*
|
|
338
|
+
* Each marquee keeps its own `playing` prop. The group's pause is an additional
|
|
339
|
+
* hold, not a replacement — a row already held still stays still.
|
|
340
|
+
*/
|
|
341
|
+
function MarqueeGroup({
|
|
342
|
+
className,
|
|
343
|
+
children,
|
|
344
|
+
playing = true,
|
|
345
|
+
showPauseControl = true,
|
|
346
|
+
pauseLabel = 'Pause',
|
|
347
|
+
playLabel = 'Play',
|
|
348
|
+
onPlayingChange,
|
|
349
|
+
...props
|
|
350
|
+
}: MarqueeGroupProps) {
|
|
351
|
+
const [userPaused, setUserPaused] = useState(false);
|
|
352
|
+
const reducedMotion = useReducedMotion();
|
|
353
|
+
|
|
354
|
+
const value = useMemo(() => ({ paused: userPaused }), [userPaused]);
|
|
355
|
+
|
|
356
|
+
// Nothing is moving, so there is nothing to stop. The control is not hidden
|
|
357
|
+
// to save space — offering to pause content that is already still is the
|
|
358
|
+
// same lie as a disabled button with no reason on it.
|
|
359
|
+
const showControl = showPauseControl && playing && !reducedMotion;
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<MarqueeGroupContext.Provider value={value}>
|
|
363
|
+
<View {...props} className={cn('w-full gap-3', className)}>
|
|
364
|
+
{children}
|
|
365
|
+
{showControl ? (
|
|
366
|
+
<Pressable
|
|
367
|
+
accessibilityRole="button"
|
|
368
|
+
accessibilityLabel={userPaused ? playLabel : pauseLabel}
|
|
369
|
+
// In flow and at the end, not floating over the last row. The
|
|
370
|
+
// control belongs to the group, and a group's control that covers
|
|
371
|
+
// one of its members is describing itself as part of that member.
|
|
372
|
+
className="min-h-12 self-end items-center justify-center rounded-full border border-border bg-background px-4"
|
|
373
|
+
onPress={() => {
|
|
374
|
+
const next = !userPaused;
|
|
375
|
+
setUserPaused(next);
|
|
376
|
+
onPlayingChange?.(!next);
|
|
377
|
+
}}
|
|
378
|
+
>
|
|
379
|
+
<Text className="text-xs font-medium text-foreground">
|
|
380
|
+
{userPaused ? playLabel : pauseLabel}
|
|
381
|
+
</Text>
|
|
382
|
+
</Pressable>
|
|
383
|
+
) : null}
|
|
384
|
+
</View>
|
|
385
|
+
</MarqueeGroupContext.Provider>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
MarqueeGroup.displayName = 'Marquee.Group';
|
|
389
|
+
|
|
390
|
+
MarqueeRoot.displayName = 'Marquee';
|
|
391
|
+
|
|
392
|
+
export const Marquee = Object.assign(MarqueeRoot, {
|
|
393
|
+
Group: MarqueeGroup,
|
|
394
|
+
});
|
|
395
|
+
|
|
273
396
|
const styles = StyleSheet.create({
|
|
274
397
|
/* Laid out so it measures, hidden so it does not draw, and behind everything
|
|
275
398
|
so it can never intercept anything. */
|
|
@@ -70,6 +70,7 @@ import { useCSSVariable } from 'uniwind';
|
|
|
70
70
|
import { Text } from '../../primitives/text';
|
|
71
71
|
import { compactNumber, useSeriesColor, wedgePath } from '../../utils/chart';
|
|
72
72
|
import { cn } from '../../utils/cn';
|
|
73
|
+
import { useSkeletonHandoff } from '../../hooks/use-skeleton-handoff';
|
|
73
74
|
|
|
74
75
|
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
75
76
|
|
|
@@ -598,10 +599,17 @@ function PieChartSkeleton({ color }: PieChartSkeletonProps) {
|
|
|
598
599
|
const token = useCSSVariable('--color-skeleton');
|
|
599
600
|
const fill = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
|
|
600
601
|
|
|
601
|
-
|
|
602
|
+
// Held through the fade rather than to the frame the data lands, so the band
|
|
603
|
+
// dissolves under the wedges sweeping over it. Cut on `status` alone, the ring
|
|
604
|
+
// vanishes before the first wedge has any sweep to show.
|
|
605
|
+
const { mounted, opacity } = useSkeletonHandoff(status === 'loading');
|
|
606
|
+
const animatedProps = useAnimatedProps(() => ({ opacity: opacity.value }));
|
|
607
|
+
|
|
608
|
+
if (!mounted || radius <= 0) return null;
|
|
602
609
|
|
|
603
610
|
return (
|
|
604
|
-
<
|
|
611
|
+
<AnimatedPath
|
|
612
|
+
animatedProps={animatedProps}
|
|
605
613
|
d={wedgePath(size / 2, size / 2, radius, hole, origin, origin + span, 0)}
|
|
606
614
|
fill={fill}
|
|
607
615
|
/>
|