@expokit/cli 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +32 -2
- package/bin/expo-kit.js +1204 -0
- package/bin/lib.mjs +343 -0
- package/bin/mcp.mjs +338 -0
- package/package.json +44 -4
- package/registry/_docs/accordion.json +99 -0
- package/registry/_docs/action-sheet.json +110 -0
- package/registry/_docs/alert.json +127 -0
- package/registry/_docs/avatar.json +203 -0
- package/registry/_docs/badge.json +117 -0
- package/registry/_docs/bottom-nav.json +119 -0
- package/registry/_docs/bottom-sheet.json +158 -0
- package/registry/_docs/bullet.json +24 -0
- package/registry/_docs/button-3d.json +122 -0
- package/registry/_docs/button.json +115 -0
- package/registry/_docs/calendar.json +182 -0
- package/registry/_docs/card-stack.json +108 -0
- package/registry/_docs/card.json +111 -0
- package/registry/_docs/carousel.json +102 -0
- package/registry/_docs/checkbox.json +93 -0
- package/registry/_docs/checkout.json +102 -0
- package/registry/_docs/chip-group.json +116 -0
- package/registry/_docs/color-swatch.json +66 -0
- package/registry/_docs/confetti.json +82 -0
- package/registry/_docs/coupon.json +176 -0
- package/registry/_docs/cube-pager.json +84 -0
- package/registry/_docs/dashboard.json +120 -0
- package/registry/_docs/date-picker.json +249 -0
- package/registry/_docs/dialog.json +162 -0
- package/registry/_docs/drawer.json +171 -0
- package/registry/_docs/fab.json +120 -0
- package/registry/_docs/flip-card.json +111 -0
- package/registry/_docs/floating-input.json +191 -0
- package/registry/_docs/gift-card.json +108 -0
- package/registry/_docs/glass-button.json +118 -0
- package/registry/_docs/glass-nav-bar.json +78 -0
- package/registry/_docs/glass-search-bar.json +90 -0
- package/registry/_docs/glass-segmented.json +66 -0
- package/registry/_docs/glass-sheet.json +108 -0
- package/registry/_docs/glass-tab-bar.json +76 -0
- package/registry/_docs/glass.json +78 -0
- package/registry/_docs/inbox.json +72 -0
- package/registry/_docs/isometric-bar.json +72 -0
- package/registry/_docs/kit.json +11 -0
- package/registry/_docs/kpi-card.json +11 -0
- package/registry/_docs/like-button.json +72 -0
- package/registry/_docs/liquidglass-tab.json +27 -0
- package/registry/_docs/list-item.json +200 -0
- package/registry/_docs/loyalty-card.json +108 -0
- package/registry/_docs/morph-button.json +108 -0
- package/registry/_docs/number-ticker.json +96 -0
- package/registry/_docs/offers.json +78 -0
- package/registry/_docs/onboarding.json +66 -0
- package/registry/_docs/otp-input.json +134 -0
- package/registry/_docs/pagination-dots.json +88 -0
- package/registry/_docs/parallax-layers.json +84 -0
- package/registry/_docs/parallax-scroll.json +186 -0
- package/registry/_docs/password-input.json +92 -0
- package/registry/_docs/payment-card.json +153 -0
- package/registry/_docs/pixel-bar.json +66 -0
- package/registry/_docs/polar-area.json +78 -0
- package/registry/_docs/profile.json +84 -0
- package/registry/_docs/progress-steps.json +92 -0
- package/registry/_docs/progress.json +132 -0
- package/registry/_docs/radio-group.json +92 -0
- package/registry/_docs/rating.json +90 -0
- package/registry/_docs/ring-carousel.json +102 -0
- package/registry/_docs/scratch-card.json +90 -0
- package/registry/_docs/search-bar.json +140 -0
- package/registry/_docs/search-results.json +120 -0
- package/registry/_docs/segmented-control.json +103 -0
- package/registry/_docs/select.json +252 -0
- package/registry/_docs/settings.json +72 -0
- package/registry/_docs/shimmer-button.json +78 -0
- package/registry/_docs/sign-in.json +198 -0
- package/registry/_docs/skeleton.json +142 -0
- package/registry/_docs/slide-to-confirm.json +84 -0
- package/registry/_docs/slider.json +157 -0
- package/registry/_docs/spin-wheel.json +114 -0
- package/registry/_docs/spinner.json +91 -0
- package/registry/_docs/stepper.json +168 -0
- package/registry/_docs/swipe-deck.json +102 -0
- package/registry/_docs/swipeable-row.json +72 -0
- package/registry/_docs/switch.json +104 -0
- package/registry/_docs/tabs.json +105 -0
- package/registry/_docs/tag-input.json +128 -0
- package/registry/_docs/textarea.json +175 -0
- package/registry/_docs/ticket-card.json +174 -0
- package/registry/_docs/ticket-shape.json +11 -0
- package/registry/_docs/tilt-card.json +141 -0
- package/registry/_docs/time-picker.json +253 -0
- package/registry/_docs/timeline-chart.json +30 -0
- package/registry/_docs/timeline.json +156 -0
- package/registry/_docs/toast.json +161 -0
- package/registry/_docs/treemap.json +30 -0
- package/registry/_docs/waterfall.json +36 -0
- package/registry/_docs/wheel-list.json +102 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
- package/registry/accordion/files/src/lib/kit.tsx +219 -0
- package/registry/accordion.json +17 -0
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
- package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
- package/registry/action-sheet.json +18 -0
- package/registry/alert/files/src/components/ui/alert.tsx +654 -0
- package/registry/alert/files/src/components/ui/button.tsx +274 -0
- package/registry/alert/files/src/lib/kit.tsx +219 -0
- package/registry/alert.json +20 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
- package/registry/avatar/files/src/lib/kit.tsx +219 -0
- package/registry/avatar.json +18 -0
- package/registry/badge/files/src/components/ui/badge.tsx +316 -0
- package/registry/badge/files/src/lib/kit.tsx +219 -0
- package/registry/badge.json +17 -0
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
- package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
- package/registry/bottom-nav.json +18 -0
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
- package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
- package/registry/bottom-sheet.json +17 -0
- package/registry/bullet.json +14 -0
- package/registry/button/files/src/components/ui/button.tsx +274 -0
- package/registry/button/files/src/lib/kit.tsx +219 -0
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
- package/registry/button-3d/files/src/lib/kit.tsx +219 -0
- package/registry/button-3d.json +18 -0
- package/registry/button.json +18 -0
- package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
- package/registry/calendar/files/src/lib/kit.tsx +219 -0
- package/registry/calendar.json +17 -0
- package/registry/card/files/src/components/ui/card.tsx +300 -0
- package/registry/card/files/src/lib/kit.tsx +219 -0
- package/registry/card-stack.json +15 -0
- package/registry/card.json +17 -0
- package/registry/carousel.json +15 -0
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
- package/registry/checkbox/files/src/lib/kit.tsx +219 -0
- package/registry/checkbox.json +17 -0
- package/registry/checkout.json +26 -0
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
- package/registry/chip-group/files/src/lib/kit.tsx +219 -0
- package/registry/chip-group.json +17 -0
- package/registry/color-swatch.json +17 -0
- package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
- package/registry/confetti/files/src/lib/kit.tsx +219 -0
- package/registry/confetti.json +15 -0
- package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
- package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
- package/registry/coupon/files/src/lib/kit.tsx +219 -0
- package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
- package/registry/coupon.json +23 -0
- package/registry/cube-pager.json +15 -0
- package/registry/dashboard.json +32 -0
- package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
- package/registry/date-picker/files/src/lib/kit.tsx +219 -0
- package/registry/date-picker.json +19 -0
- package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
- package/registry/dialog/files/src/lib/kit.tsx +219 -0
- package/registry/dialog.json +18 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
- package/registry/drawer/files/src/lib/kit.tsx +219 -0
- package/registry/drawer.json +18 -0
- package/registry/fab.json +18 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
- package/registry/flip-card/files/src/lib/kit.tsx +219 -0
- package/registry/flip-card.json +15 -0
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
- package/registry/floating-input/files/src/lib/kit.tsx +219 -0
- package/registry/floating-input.json +17 -0
- package/registry/gift-card.json +21 -0
- package/registry/glass-button.json +22 -0
- package/registry/glass-nav-bar.json +21 -0
- package/registry/glass-search-bar.json +21 -0
- package/registry/glass-segmented.json +22 -0
- package/registry/glass-sheet.json +22 -0
- package/registry/glass-tab-bar.json +22 -0
- package/registry/glass.json +19 -0
- package/registry/inbox.json +30 -0
- package/registry/isometric-bar.json +14 -0
- package/registry/kit/files/src/lib/kit.tsx +219 -0
- package/registry/kit.json +11 -0
- package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
- package/registry/kpi-card.json +14 -0
- package/registry/like-button.json +18 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
- package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
- package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
- package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
- package/registry/liquidglass-tab.json +23 -0
- package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
- package/registry/list-item/files/src/lib/kit.tsx +219 -0
- package/registry/list-item.json +17 -0
- package/registry/loyalty-card.json +23 -0
- package/registry/morph-button.json +18 -0
- package/registry/number-ticker.json +15 -0
- package/registry/offers.json +24 -0
- package/registry/onboarding.json +17 -0
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
- package/registry/otp-input/files/src/lib/kit.tsx +219 -0
- package/registry/otp-input.json +17 -0
- package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
- package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
- package/registry/pagination-dots.json +15 -0
- package/registry/parallax-layers.json +15 -0
- package/registry/parallax-scroll.json +17 -0
- package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
- package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
- package/registry/password-input/files/src/lib/kit.tsx +219 -0
- package/registry/password-input.json +19 -0
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
- package/registry/payment-card/files/src/lib/kit.tsx +219 -0
- package/registry/payment-card.json +21 -0
- package/registry/pixel-bar.json +14 -0
- package/registry/polar-area.json +14 -0
- package/registry/profile.json +17 -0
- package/registry/progress/files/src/components/ui/progress.tsx +883 -0
- package/registry/progress/files/src/lib/kit.tsx +219 -0
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
- package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
- package/registry/progress-steps.json +18 -0
- package/registry/progress.json +17 -0
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
- package/registry/radio-group/files/src/lib/kit.tsx +219 -0
- package/registry/radio-group.json +18 -0
- package/registry/rating.json +18 -0
- package/registry/ring-carousel.json +15 -0
- package/registry/scratch-card.json +18 -0
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
- package/registry/search-bar/files/src/lib/kit.tsx +219 -0
- package/registry/search-bar.json +17 -0
- package/registry/search-results.json +30 -0
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
- package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
- package/registry/segmented-control.json +18 -0
- package/registry/select/files/src/components/ui/select.tsx +1398 -0
- package/registry/select/files/src/lib/kit.tsx +219 -0
- package/registry/select.json +17 -0
- package/registry/settings.json +26 -0
- package/registry/shimmer-button.json +17 -0
- package/registry/sign-in.json +26 -0
- package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
- package/registry/skeleton/files/src/lib/kit.tsx +219 -0
- package/registry/skeleton.json +17 -0
- package/registry/slide-to-confirm.json +18 -0
- package/registry/slider/files/src/components/ui/slider.tsx +716 -0
- package/registry/slider/files/src/lib/kit.tsx +219 -0
- package/registry/slider.json +17 -0
- package/registry/spin-wheel.json +18 -0
- package/registry/spinner/files/src/components/ui/spinner.tsx +335 -0
- package/registry/spinner/files/src/lib/kit.tsx +219 -0
- package/registry/spinner.json +17 -0
- package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
- package/registry/stepper/files/src/lib/kit.tsx +219 -0
- package/registry/stepper.json +17 -0
- package/registry/swipe-deck.json +15 -0
- package/registry/swipeable-row.json +15 -0
- package/registry/switch/files/src/components/ui/switch.tsx +344 -0
- package/registry/switch/files/src/lib/kit.tsx +219 -0
- package/registry/switch.json +18 -0
- package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
- package/registry/tabs/files/src/lib/kit.tsx +219 -0
- package/registry/tabs.json +17 -0
- package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
- package/registry/tag-input/files/src/lib/kit.tsx +219 -0
- package/registry/tag-input.json +17 -0
- package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
- package/registry/textarea/files/src/lib/kit.tsx +219 -0
- package/registry/textarea.json +17 -0
- package/registry/ticket-card.json +24 -0
- package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
- package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
- package/registry/ticket-shape.json +17 -0
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
- package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
- package/registry/tilt-card.json +17 -0
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
- package/registry/time-picker/files/src/lib/kit.tsx +219 -0
- package/registry/time-picker.json +17 -0
- package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
- package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
- package/registry/timeline/files/src/lib/kit.tsx +219 -0
- package/registry/timeline-chart.json +14 -0
- package/registry/timeline.json +20 -0
- package/registry/toast/files/src/components/ui/button.tsx +274 -0
- package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
- package/registry/toast/files/src/lib/kit.tsx +219 -0
- package/registry/toast.json +20 -0
- package/registry/treemap.json +14 -0
- package/registry/waterfall.json +14 -0
- package/registry/wheel-list.json +17 -0
- package/schema/components.schema.json +13 -0
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
PanResponder,
|
|
5
|
+
Pressable,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Text,
|
|
8
|
+
View,
|
|
9
|
+
type AccessibilityActionEvent,
|
|
10
|
+
type GestureResponderEvent,
|
|
11
|
+
type StyleProp,
|
|
12
|
+
type TextStyle,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
import Svg, { Circle, Path } from 'react-native-svg';
|
|
16
|
+
|
|
17
|
+
import { alpha, motion, readableOn, useKit } from '@/lib/kit';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* - `default` — thin track with a round thumb (iOS / Material).
|
|
21
|
+
* - `bar` — a tall rounded bar with a flat fill and no thumb, like the iOS Control Center volume
|
|
22
|
+
* bar. Put an `icon` (and the value, with `showValue="always"`) inside it.
|
|
23
|
+
* - `stepped` — a slightly thicker track with a dot at every step; the thumb snaps between them.
|
|
24
|
+
*/
|
|
25
|
+
export type SliderVariant = 'default' | 'bar' | 'stepped';
|
|
26
|
+
export type SliderSize = 'sm' | 'md' | 'lg';
|
|
27
|
+
export type SliderValue = number | [number, number];
|
|
28
|
+
export type SliderMark = number | { value: number; label?: string };
|
|
29
|
+
/** Built-in icons for `leftIcon` / `rightIcon` / `icon`. */
|
|
30
|
+
export type SliderIconName = 'volume-low' | 'volume-high' | 'volume-off' | 'sun-low' | 'sun-high' | 'minus' | 'plus';
|
|
31
|
+
/** A built-in icon name, any node, or a function that receives the colour to draw in. */
|
|
32
|
+
export type SliderIconProp = SliderIconName | React.ReactNode | ((color: string) => React.ReactNode);
|
|
33
|
+
|
|
34
|
+
interface SliderBaseProps {
|
|
35
|
+
min?: number;
|
|
36
|
+
max?: number;
|
|
37
|
+
/** Snap interval. 0 makes the slider continuous. */
|
|
38
|
+
step?: number;
|
|
39
|
+
/** See `SliderVariant`. */
|
|
40
|
+
variant?: SliderVariant;
|
|
41
|
+
size?: SliderSize;
|
|
42
|
+
/** `true` draws a dot at every step (default for `stepped`); or pass values with optional labels. */
|
|
43
|
+
marks?: boolean | SliderMark[];
|
|
44
|
+
/**
|
|
45
|
+
* Value tooltip above the thumb: only while dragging, always, or never.
|
|
46
|
+
* Default `drag`, or `never` for the `bar` variant — where `always` shows the value inside the bar.
|
|
47
|
+
*/
|
|
48
|
+
showValue?: 'drag' | 'always' | 'never';
|
|
49
|
+
formatValue?: (value: number) => string;
|
|
50
|
+
/** Shows the formatted min and max under the track. */
|
|
51
|
+
showMinMax?: boolean;
|
|
52
|
+
/** Custom text under the start of the track (implies `showMinMax`). */
|
|
53
|
+
minLabel?: string;
|
|
54
|
+
/** Custom text under the end of the track (implies `showMinMax`). */
|
|
55
|
+
maxLabel?: string;
|
|
56
|
+
/** Icon before the track. Built-in names step the value down when tapped. */
|
|
57
|
+
leftIcon?: SliderIconProp;
|
|
58
|
+
/** Icon after the track. Built-in names step the value up when tapped. */
|
|
59
|
+
rightIcon?: SliderIconProp;
|
|
60
|
+
/** `bar` only: an icon inside the start of the bar. It changes colour as the fill passes over it. */
|
|
61
|
+
icon?: SliderIconProp;
|
|
62
|
+
/** Draws the active fill thicker than the track (`default` variant). */
|
|
63
|
+
thickFill?: boolean;
|
|
64
|
+
/** Bar heights drawn above the track (e.g. price distribution). Bars inside the selection use the accent. */
|
|
65
|
+
histogram?: number[];
|
|
66
|
+
/** Height of the histogram in points. */
|
|
67
|
+
histogramHeight?: number;
|
|
68
|
+
disabled?: boolean;
|
|
69
|
+
/** Fill colour (default: theme accent). */
|
|
70
|
+
accent?: string;
|
|
71
|
+
accessibilityLabel?: string;
|
|
72
|
+
style?: StyleProp<ViewStyle>;
|
|
73
|
+
/** The inactive track. */
|
|
74
|
+
trackStyle?: StyleProp<ViewStyle>;
|
|
75
|
+
/** The active fill (e.g. `{ backgroundColor }`). */
|
|
76
|
+
fillStyle?: StyleProp<ViewStyle>;
|
|
77
|
+
/** The round thumb (`default` / `stepped`). */
|
|
78
|
+
thumbStyle?: StyleProp<ViewStyle>;
|
|
79
|
+
/** The value text: tooltip, in-bar value and min / max labels. */
|
|
80
|
+
labelStyle?: StyleProp<TextStyle>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One thumb: `value` is a number. */
|
|
84
|
+
export interface SliderSingleProps extends SliderBaseProps {
|
|
85
|
+
range?: false;
|
|
86
|
+
value?: number;
|
|
87
|
+
defaultValue?: number;
|
|
88
|
+
/** Fires whenever the (stepped) value changes while dragging, tapping or via accessibility actions. */
|
|
89
|
+
onValueChange?: (value: number) => void;
|
|
90
|
+
onSlidingStart?: (value: number) => void;
|
|
91
|
+
onSlidingComplete?: (value: number) => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Two thumbs selecting a span: `value` is `[from, to]`. */
|
|
95
|
+
export interface SliderRangeProps extends SliderBaseProps {
|
|
96
|
+
range: true;
|
|
97
|
+
value?: [number, number];
|
|
98
|
+
defaultValue?: [number, number];
|
|
99
|
+
onValueChange?: (value: [number, number]) => void;
|
|
100
|
+
onSlidingStart?: (value: [number, number]) => void;
|
|
101
|
+
onSlidingComplete?: (value: [number, number]) => void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type SliderProps = SliderSingleProps | SliderRangeProps;
|
|
105
|
+
|
|
106
|
+
type Callback = ((value: SliderValue) => void) | undefined;
|
|
107
|
+
|
|
108
|
+
const SIZES: Record<SliderSize, { track: number; stepped: number; thumb: number; bar: number; icon: number; font: number; hit: number }> = {
|
|
109
|
+
sm: { track: 3, stepped: 6, thumb: 20, bar: 36, icon: 16, font: 12, hit: 44 },
|
|
110
|
+
md: { track: 4, stepped: 8, thumb: 26, bar: 46, icon: 20, font: 13, hit: 44 },
|
|
111
|
+
lg: { track: 6, stepped: 10, thumb: 30, bar: 58, icon: 24, font: 15, hit: 52 },
|
|
112
|
+
};
|
|
113
|
+
const BUBBLE_H = 28;
|
|
114
|
+
const BUBBLE_GAP = 10;
|
|
115
|
+
const BUBBLE_W = 120;
|
|
116
|
+
/** Below this many points per step the thumb follows the finger and snaps on release. */
|
|
117
|
+
const SNAP_PX = 14;
|
|
118
|
+
|
|
119
|
+
const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));
|
|
120
|
+
const decimals = (n: number) => (String(n).split('.')[1] ?? '').length;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A slider that feels native: the thumb and fill move on the native driver (no re-render per
|
|
124
|
+
* frame), the thumb swells under your finger, tap anywhere on the track to seek, and values snap
|
|
125
|
+
* to `step` with a spring. Variants: a thin iOS track, a Control-Center style `bar`, and a
|
|
126
|
+
* `stepped` track with ticks; `range` adds a second thumb. Optional value tooltip, min / max
|
|
127
|
+
* labels, icons and a histogram for filters.
|
|
128
|
+
*/
|
|
129
|
+
export function Slider(props: SliderProps) {
|
|
130
|
+
const {
|
|
131
|
+
min = 0,
|
|
132
|
+
max = 100,
|
|
133
|
+
step = 1,
|
|
134
|
+
variant = 'default',
|
|
135
|
+
size = 'md',
|
|
136
|
+
marks,
|
|
137
|
+
formatValue,
|
|
138
|
+
showMinMax,
|
|
139
|
+
minLabel,
|
|
140
|
+
maxLabel,
|
|
141
|
+
leftIcon,
|
|
142
|
+
rightIcon,
|
|
143
|
+
icon,
|
|
144
|
+
thickFill,
|
|
145
|
+
histogram,
|
|
146
|
+
histogramHeight = 44,
|
|
147
|
+
disabled,
|
|
148
|
+
accent,
|
|
149
|
+
accessibilityLabel,
|
|
150
|
+
style,
|
|
151
|
+
trackStyle,
|
|
152
|
+
fillStyle,
|
|
153
|
+
thumbStyle,
|
|
154
|
+
labelStyle,
|
|
155
|
+
} = props;
|
|
156
|
+
const range = props.range === true;
|
|
157
|
+
const isBar = variant === 'bar';
|
|
158
|
+
const stepped = variant === 'stepped';
|
|
159
|
+
const showValue = props.showValue ?? (isBar ? 'never' : 'drag');
|
|
160
|
+
const onValueChange = props.onValueChange as Callback;
|
|
161
|
+
const onSlidingStart = props.onSlidingStart as Callback;
|
|
162
|
+
const onSlidingComplete = props.onSlidingComplete as Callback;
|
|
163
|
+
|
|
164
|
+
const { colors, radius, dark } = useKit();
|
|
165
|
+
const tint = accent ?? colors.accent;
|
|
166
|
+
const onTint = accent ? readableOn(accent) : colors.accentForeground;
|
|
167
|
+
const sz = SIZES[size];
|
|
168
|
+
|
|
169
|
+
const T = isBar ? 0 : sz.thumb; // thumb diameter
|
|
170
|
+
const inset = T / 2; // thumb centre travels [inset, w - inset]
|
|
171
|
+
const trackH = isBar ? sz.bar : stepped ? sz.stepped : sz.track;
|
|
172
|
+
const fillH = !isBar && !stepped && thickFill ? trackH + 4 : trackH;
|
|
173
|
+
const surfaceH = Math.max(sz.hit, isBar ? sz.bar : T);
|
|
174
|
+
const slotW = Math.max(T, 2);
|
|
175
|
+
const histH = histogram?.length ? histogramHeight + 8 : 0;
|
|
176
|
+
const bubbleBottom = surfaceH / 2 + (isBar ? trackH / 2 : T / 2) + BUBBLE_GAP;
|
|
177
|
+
const bubbleShown = showValue === 'always' && !isBar;
|
|
178
|
+
// Only an always-visible tooltip needs room above the track; a drag tooltip floats over whatever is there.
|
|
179
|
+
const reserve = bubbleShown ? Math.max(0, bubbleBottom + BUBBLE_H - surfaceH - histH) : 0;
|
|
180
|
+
|
|
181
|
+
// ---- values ---------------------------------------------------------------------------------
|
|
182
|
+
const toArr = (v: SliderValue | undefined): number[] => {
|
|
183
|
+
if (Array.isArray(v)) return range ? [v[0], v[1]] : [v[0]];
|
|
184
|
+
if (typeof v === 'number') return range ? [min, v] : [v];
|
|
185
|
+
return range ? [min, max] : [min];
|
|
186
|
+
};
|
|
187
|
+
const controlled = props.value !== undefined;
|
|
188
|
+
const [inner, setInner] = useState<number[]>(() => toArr(props.defaultValue));
|
|
189
|
+
const vals = (controlled ? toArr(props.value) : inner).map((v) => clamp(v, min, max));
|
|
190
|
+
if (range && vals[0] > vals[1]) vals.reverse();
|
|
191
|
+
|
|
192
|
+
const span = max - min || 1;
|
|
193
|
+
const ratioOf = (v: number) => clamp((v - min) / span, 0, 1);
|
|
194
|
+
const snap = (v: number) => {
|
|
195
|
+
if (!step) return clamp(v, min, max);
|
|
196
|
+
const n = Math.round((v - min) / step) * step + min;
|
|
197
|
+
return clamp(Number(n.toFixed(decimals(step))), min, max);
|
|
198
|
+
};
|
|
199
|
+
const fmt = (v: number) => (formatValue ? formatValue(v) : step ? v.toFixed(decimals(step)) : String(Math.round(v)));
|
|
200
|
+
const out = (arr: number[]): SliderValue => (range ? [arr[0], arr[1]] : arr[0]);
|
|
201
|
+
|
|
202
|
+
const [w, setW] = useState(0);
|
|
203
|
+
const [active, setActive] = useState<number | null>(null);
|
|
204
|
+
|
|
205
|
+
// ---- animated values ------------------------------------------------------------------------
|
|
206
|
+
// All native-driven: they only ever feed transform / opacity.
|
|
207
|
+
const ratios = useRef([new Animated.Value(ratioOf(vals[0])), new Animated.Value(ratioOf(vals[1] ?? vals[0]))]).current;
|
|
208
|
+
const press = useRef([new Animated.Value(0), new Animated.Value(0)]).current;
|
|
209
|
+
const bubble = useRef([new Animated.Value(bubbleShown ? 1 : 0), new Animated.Value(bubbleShown ? 1 : 0)]).current;
|
|
210
|
+
const held = useRef(new Animated.Value(0)).current;
|
|
211
|
+
|
|
212
|
+
// Everything the (once-created) PanResponder reads, kept fresh every render.
|
|
213
|
+
const live = useRef({ vals, w, inset, step, span, min, range, isBar, T, disabled, controlled, snap, ratioOf, fmt, out, onValueChange, onSlidingStart, onSlidingComplete });
|
|
214
|
+
live.current = { vals, w, inset, step, span, min, range, isBar, T, disabled, controlled, snap, ratioOf, fmt, out, onValueChange, onSlidingStart, onSlidingComplete };
|
|
215
|
+
const drag = useRef({ dragging: false, thumb: 0, startX: 0, origin: 0, relative: false, moved: false, cur: [] as number[], target: [-1, -1] });
|
|
216
|
+
|
|
217
|
+
// Follow value changes from outside (controlled updates, a11y, icon taps) when nobody is dragging.
|
|
218
|
+
const key = vals.join(',');
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (drag.current.dragging) return;
|
|
221
|
+
vals.forEach((v, i) => Animated.spring(ratios[i], { toValue: ratioOf(v), useNativeDriver: true, ...motion.snappy }).start());
|
|
222
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
223
|
+
}, [key, min, max]);
|
|
224
|
+
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
press.forEach((p, i) => Animated.spring(p, { toValue: active === i ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start());
|
|
227
|
+
bubble.forEach((b, i) =>
|
|
228
|
+
Animated.spring(b, {
|
|
229
|
+
toValue: bubbleShown || (showValue === 'drag' && active === i) ? 1 : 0,
|
|
230
|
+
useNativeDriver: true,
|
|
231
|
+
...motion.snappy,
|
|
232
|
+
}).start()
|
|
233
|
+
);
|
|
234
|
+
Animated.spring(held, { toValue: active !== null ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
235
|
+
}, [active, bubbleShown, showValue, press, bubble, held]);
|
|
236
|
+
|
|
237
|
+
const pan = useMemo(() => {
|
|
238
|
+
const usableOf = () => Math.max(1, live.current.w - 2 * live.current.inset);
|
|
239
|
+
const ratioAt = (x: number) => clamp((x - live.current.inset) / usableOf(), 0, 1);
|
|
240
|
+
|
|
241
|
+
const moveTo = (x: number, animate: boolean) => {
|
|
242
|
+
const L = live.current;
|
|
243
|
+
const d = drag.current;
|
|
244
|
+
const i = d.thumb;
|
|
245
|
+
let v = L.min + ratioAt(x) * L.span;
|
|
246
|
+
if (L.range) v = i === 0 ? Math.min(v, d.cur[1]) : Math.max(v, d.cur[0]);
|
|
247
|
+
const snapped = L.snap(v);
|
|
248
|
+
const coarse = !!L.step && (L.step / L.span) * usableOf() >= SNAP_PX;
|
|
249
|
+
const target = coarse ? L.ratioOf(snapped) : L.ratioOf(v);
|
|
250
|
+
if (animate || coarse) {
|
|
251
|
+
if (target !== d.target[i]) Animated.spring(ratios[i], { toValue: target, useNativeDriver: true, ...motion.snappy }).start();
|
|
252
|
+
} else {
|
|
253
|
+
ratios[i].setValue(target);
|
|
254
|
+
}
|
|
255
|
+
d.target[i] = target;
|
|
256
|
+
if (snapped === d.cur[i]) return;
|
|
257
|
+
const prev = d.cur[i];
|
|
258
|
+
const next = [...d.cur];
|
|
259
|
+
next[i] = snapped;
|
|
260
|
+
d.cur = next;
|
|
261
|
+
// Uncontrolled continuous sliders only re-render when the shown text changes.
|
|
262
|
+
if (!L.controlled && (L.step || L.fmt(prev) !== L.fmt(snapped))) setInner(next);
|
|
263
|
+
L.onValueChange?.(L.out(next));
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const finish = (seek: boolean) => {
|
|
267
|
+
const L = live.current;
|
|
268
|
+
const d = drag.current;
|
|
269
|
+
if (!d.dragging) return;
|
|
270
|
+
if (seek) moveTo(d.startX, true);
|
|
271
|
+
Animated.spring(ratios[d.thumb], { toValue: L.ratioOf(d.cur[d.thumb]), useNativeDriver: true, ...motion.snappy }).start();
|
|
272
|
+
d.dragging = false;
|
|
273
|
+
d.target = [-1, -1];
|
|
274
|
+
if (!L.controlled) setInner(d.cur);
|
|
275
|
+
setActive(null);
|
|
276
|
+
L.onSlidingComplete?.(L.out(d.cur));
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return PanResponder.create({
|
|
280
|
+
onStartShouldSetPanResponder: () => !live.current.disabled,
|
|
281
|
+
onMoveShouldSetPanResponder: () => !live.current.disabled,
|
|
282
|
+
onPanResponderTerminationRequest: () => false,
|
|
283
|
+
onShouldBlockNativeResponder: () => true,
|
|
284
|
+
onPanResponderGrant: (e: GestureResponderEvent) => {
|
|
285
|
+
const L = live.current;
|
|
286
|
+
const x = e.nativeEvent.locationX;
|
|
287
|
+
const cur = [...L.vals];
|
|
288
|
+
const xs = cur.map((v) => L.inset + L.ratioOf(v) * usableOf());
|
|
289
|
+
let thumb = 0;
|
|
290
|
+
if (L.range) {
|
|
291
|
+
const d0 = Math.abs(x - xs[0]);
|
|
292
|
+
const d1 = Math.abs(x - xs[1]);
|
|
293
|
+
thumb = d0 === d1 ? (x > xs[1] ? 1 : 0) : d1 < d0 ? 1 : 0;
|
|
294
|
+
}
|
|
295
|
+
// Grabbing the thumb (or anywhere on a bar) drags relative to it; elsewhere seeks first.
|
|
296
|
+
const onThumb = !L.isBar && Math.abs(x - xs[thumb]) <= Math.max(L.T / 2 + 8, 22);
|
|
297
|
+
const relative = L.isBar || onThumb;
|
|
298
|
+
drag.current = { dragging: true, thumb, startX: x, origin: relative ? xs[thumb] : x, relative, moved: false, cur, target: [-1, -1] };
|
|
299
|
+
setActive(thumb);
|
|
300
|
+
L.onSlidingStart?.(L.out(cur));
|
|
301
|
+
if (!relative) moveTo(x, true);
|
|
302
|
+
},
|
|
303
|
+
onPanResponderMove: (_, g) => {
|
|
304
|
+
const d = drag.current;
|
|
305
|
+
if (Math.abs(g.dx) >= 3) d.moved = true;
|
|
306
|
+
if (!d.moved && live.current.isBar) return; // keep a tap on a bar a tap
|
|
307
|
+
moveTo(d.origin + g.dx, false);
|
|
308
|
+
},
|
|
309
|
+
// A tap on a bar (no drag) still seeks.
|
|
310
|
+
onPanResponderRelease: () => finish(drag.current.relative && live.current.isBar && !drag.current.moved),
|
|
311
|
+
onPanResponderTerminate: () => finish(false),
|
|
312
|
+
});
|
|
313
|
+
}, [ratios]);
|
|
314
|
+
|
|
315
|
+
const stepBy = (i: number, dir: 1 | -1, amount?: number) => {
|
|
316
|
+
if (disabled) return;
|
|
317
|
+
const inc = amount ?? (step || span / 100);
|
|
318
|
+
const next = [...vals];
|
|
319
|
+
next[i] = snap(next[i] + dir * inc);
|
|
320
|
+
if (range) next[i] = i === 0 ? Math.min(next[i], vals[1]) : Math.max(next[i], vals[0]);
|
|
321
|
+
if (next[i] === vals[i]) return;
|
|
322
|
+
if (!controlled) setInner(next);
|
|
323
|
+
onValueChange?.(out(next));
|
|
324
|
+
onSlidingComplete?.(out(next));
|
|
325
|
+
};
|
|
326
|
+
// Tapping a built-in side icon moves by roughly a tenth of the range, rounded to the step.
|
|
327
|
+
const iconStep = step ? Math.max(step, Math.round(span / 10 / step) * step) : span / 10;
|
|
328
|
+
|
|
329
|
+
// ---- animated styles (memoised so re-renders don't rebuild native nodes) ---------------------
|
|
330
|
+
const usable = Math.max(1, w - 2 * inset);
|
|
331
|
+
const geo = useMemo(() => {
|
|
332
|
+
if (!w) return null;
|
|
333
|
+
const px = ratios.map((r) =>
|
|
334
|
+
r.interpolate({ inputRange: [0, 1], outputRange: [inset, inset + usable], extrapolate: 'clamp' })
|
|
335
|
+
);
|
|
336
|
+
// Single: a full-width fill slid left so its edge sits at the thumb. Range: a full-width fill
|
|
337
|
+
// scaled about its centre to span the two thumbs. Both are clipped by a rounded container.
|
|
338
|
+
const shift = Animated.add(px[0], -w);
|
|
339
|
+
const fill = range
|
|
340
|
+
? [
|
|
341
|
+
{ translateX: Animated.add(Animated.multiply(Animated.add(px[0], px[1]), 0.5), -w / 2) },
|
|
342
|
+
{ scaleX: Animated.multiply(Animated.subtract(px[1], px[0]), 1 / w) },
|
|
343
|
+
]
|
|
344
|
+
: [{ translateX: shift }];
|
|
345
|
+
return {
|
|
346
|
+
fill,
|
|
347
|
+
counter: [{ translateX: Animated.multiply(shift, -1) }], // keeps in-bar content still while the fill slides
|
|
348
|
+
slot: px.map((p) => [{ translateX: Animated.add(p, -slotW / 2) }]),
|
|
349
|
+
};
|
|
350
|
+
}, [w, inset, usable, range, slotW, ratios]);
|
|
351
|
+
|
|
352
|
+
const anim = useMemo(
|
|
353
|
+
() => ({
|
|
354
|
+
thumb: press.map((p) => [{ scale: p.interpolate({ inputRange: [0, 1], outputRange: [1, 1.14] }) }]),
|
|
355
|
+
halo: press.map((p) => [{ scale: p.interpolate({ inputRange: [0, 1], outputRange: [1, 1.75] }) }]),
|
|
356
|
+
bubble: bubble.map((b) => [
|
|
357
|
+
{ translateY: b.interpolate({ inputRange: [0, 1], outputRange: [6, 0] }) },
|
|
358
|
+
{ scale: b.interpolate({ inputRange: [0, 1], outputRange: [0.6, 1] }) },
|
|
359
|
+
]),
|
|
360
|
+
held: [{ scale: held.interpolate({ inputRange: [0, 1], outputRange: [1, 1.025] }) }],
|
|
361
|
+
}),
|
|
362
|
+
[press, bubble, held]
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// ---- marks ----------------------------------------------------------------------------------
|
|
366
|
+
const markList: { value: number; label?: string }[] = useMemo(() => {
|
|
367
|
+
const m = marks ?? (stepped ? true : undefined);
|
|
368
|
+
if (!m) return [];
|
|
369
|
+
if (m === true) {
|
|
370
|
+
if (!step || span / step > 40) return [];
|
|
371
|
+
return Array.from({ length: Math.round(span / step) + 1 }, (_, i) => ({ value: Number((min + i * step).toFixed(decimals(step))) }));
|
|
372
|
+
}
|
|
373
|
+
return m.map((x) => (typeof x === 'number' ? { value: x } : x));
|
|
374
|
+
}, [marks, stepped, step, span, min]);
|
|
375
|
+
const hasMarkLabels = markList.some((m) => m.label);
|
|
376
|
+
const minMax = showMinMax || minLabel !== undefined || maxLabel !== undefined;
|
|
377
|
+
const inFill = (v: number) => (range ? v >= vals[0] && v <= vals[1] : v <= vals[0]);
|
|
378
|
+
const dot = Math.max(2, Math.round(trackH * (isBar ? 0.12 : 0.4)));
|
|
379
|
+
|
|
380
|
+
// ---- colours --------------------------------------------------------------------------------
|
|
381
|
+
const trackColor = alpha(colors.mutedForeground, isBar ? (dark ? 0.24 : 0.14) : dark ? 0.3 : 0.2);
|
|
382
|
+
const thumbColor = dark ? colors.foreground : colors.card;
|
|
383
|
+
|
|
384
|
+
const drawIcon = (ic: SliderIconProp, color: string) => {
|
|
385
|
+
if (typeof ic === 'function') return ic(color);
|
|
386
|
+
if (typeof ic === 'string') return <SliderIcon name={ic as SliderIconName} size={isBar ? sz.icon + 2 : sz.icon} color={color} />;
|
|
387
|
+
return ic;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const sideIcon = (ic: SliderIconProp | undefined, dir: 1 | -1) => {
|
|
391
|
+
if (ic === undefined || ic === null || ic === false) return null;
|
|
392
|
+
const node = drawIcon(ic, colors.mutedForeground);
|
|
393
|
+
const box = [styles.iconSlot, { height: surfaceH, marginTop: histH }];
|
|
394
|
+
if (typeof ic !== 'string') return <View style={box}>{node}</View>;
|
|
395
|
+
return (
|
|
396
|
+
<Pressable
|
|
397
|
+
accessible={false}
|
|
398
|
+
disabled={disabled}
|
|
399
|
+
hitSlop={10}
|
|
400
|
+
onPress={() => stepBy(dir === 1 ? vals.length - 1 : 0, dir, iconStep)}
|
|
401
|
+
style={({ pressed }) => [box, { opacity: pressed ? 0.45 : 1 }]}
|
|
402
|
+
>
|
|
403
|
+
{node}
|
|
404
|
+
</Pressable>
|
|
405
|
+
);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Content drawn inside a bar: icon at the start, value at the end. Rendered twice — once over
|
|
409
|
+
// the track and once inside the fill in the "on accent" colour — so it inverts as the fill passes.
|
|
410
|
+
const barInline = isBar && (icon !== undefined || showValue === 'always');
|
|
411
|
+
const barContent = (iconColor: string, textColor: string) => (
|
|
412
|
+
<View style={[styles.barContent, { width: w, height: trackH, paddingHorizontal: Math.round(trackH * 0.32) }]}>
|
|
413
|
+
<View>{icon !== undefined ? drawIcon(icon, iconColor) : null}</View>
|
|
414
|
+
{showValue === 'always' ? (
|
|
415
|
+
<Text numberOfLines={1} selectable={false} style={[styles.barValue, { color: textColor, fontSize: sz.font + 1 }, labelStyle]}>
|
|
416
|
+
{range ? `${fmt(vals[0])} – ${fmt(vals[1])}` : fmt(vals[0])}
|
|
417
|
+
</Text>
|
|
418
|
+
) : null}
|
|
419
|
+
</View>
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const labelText = (key: string, text: string, lit: boolean, place: TextStyle) => (
|
|
423
|
+
<Text
|
|
424
|
+
key={key}
|
|
425
|
+
numberOfLines={1}
|
|
426
|
+
selectable={false}
|
|
427
|
+
style={[styles.markLabel, place, { fontSize: sz.font - 1, color: lit ? colors.foreground : colors.mutedForeground }, labelStyle]}
|
|
428
|
+
>
|
|
429
|
+
{text}
|
|
430
|
+
</Text>
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
return (
|
|
434
|
+
<View
|
|
435
|
+
style={[styles.root, { paddingTop: reserve, opacity: disabled ? 0.45 : 1 }, style]}
|
|
436
|
+
accessible={!range}
|
|
437
|
+
accessibilityRole={!range ? 'adjustable' : undefined}
|
|
438
|
+
accessibilityLabel={accessibilityLabel}
|
|
439
|
+
accessibilityState={{ disabled: !!disabled }}
|
|
440
|
+
accessibilityValue={!range ? { min, max, now: vals[0], text: fmt(vals[0]) } : undefined}
|
|
441
|
+
accessibilityActions={!range ? [{ name: 'increment' }, { name: 'decrement' }] : undefined}
|
|
442
|
+
onAccessibilityAction={(e: AccessibilityActionEvent) => stepBy(0, e.nativeEvent.actionName === 'increment' ? 1 : -1)}
|
|
443
|
+
>
|
|
444
|
+
<View style={styles.row}>
|
|
445
|
+
{sideIcon(leftIcon, -1)}
|
|
446
|
+
<View style={styles.middle}>
|
|
447
|
+
{histogram?.length ? (
|
|
448
|
+
<View pointerEvents="none" style={[styles.histogram, { height: histogramHeight, paddingHorizontal: inset }]}>
|
|
449
|
+
{histogram.map((h, i, all) => {
|
|
450
|
+
const top = Math.max(...all, 1);
|
|
451
|
+
const mid = min + ((i + 0.5) / histogram.length) * span;
|
|
452
|
+
return (
|
|
453
|
+
<View
|
|
454
|
+
key={i}
|
|
455
|
+
style={[
|
|
456
|
+
styles.histBar,
|
|
457
|
+
{
|
|
458
|
+
height: Math.max(2, (h / top) * histogramHeight),
|
|
459
|
+
borderTopLeftRadius: radius.scale(3),
|
|
460
|
+
borderTopRightRadius: radius.scale(3),
|
|
461
|
+
backgroundColor: inFill(mid) ? alpha(tint, 0.8) : alpha(colors.mutedForeground, 0.2),
|
|
462
|
+
},
|
|
463
|
+
]}
|
|
464
|
+
/>
|
|
465
|
+
);
|
|
466
|
+
})}
|
|
467
|
+
</View>
|
|
468
|
+
) : null}
|
|
469
|
+
|
|
470
|
+
<View
|
|
471
|
+
{...pan.panHandlers}
|
|
472
|
+
onLayout={(e) => setW(e.nativeEvent.layout.width)}
|
|
473
|
+
style={[styles.surface, { height: surfaceH }, !disabled && styles.pointer]}
|
|
474
|
+
>
|
|
475
|
+
<Animated.View pointerEvents="none" style={[styles.layer, isBar && { transform: anim.held }]}>
|
|
476
|
+
{/* Inactive track */}
|
|
477
|
+
<View
|
|
478
|
+
style={[
|
|
479
|
+
styles.track,
|
|
480
|
+
{ top: (surfaceH - trackH) / 2, height: trackH, borderRadius: radius.capsule(trackH), backgroundColor: trackColor },
|
|
481
|
+
trackStyle,
|
|
482
|
+
]}
|
|
483
|
+
>
|
|
484
|
+
{barInline && w ? barContent(colors.mutedForeground, colors.foreground) : null}
|
|
485
|
+
</View>
|
|
486
|
+
|
|
487
|
+
{/* Active fill, clipped to the track shape */}
|
|
488
|
+
{geo ? (
|
|
489
|
+
<View style={[styles.clip, { top: (surfaceH - fillH) / 2, width: w, height: fillH, borderRadius: radius.capsule(fillH) }]}>
|
|
490
|
+
<Animated.View style={[styles.fill, { width: w, backgroundColor: tint, transform: geo.fill }, fillStyle]}>
|
|
491
|
+
{barInline && !range ? (
|
|
492
|
+
<Animated.View style={[styles.counter, { transform: geo.counter }]}>{barContent(onTint, onTint)}</Animated.View>
|
|
493
|
+
) : null}
|
|
494
|
+
</Animated.View>
|
|
495
|
+
</View>
|
|
496
|
+
) : null}
|
|
497
|
+
|
|
498
|
+
{/* Step ticks */}
|
|
499
|
+
{w
|
|
500
|
+
? markList.map((m) => (
|
|
501
|
+
<View
|
|
502
|
+
key={m.value}
|
|
503
|
+
style={[
|
|
504
|
+
styles.tick,
|
|
505
|
+
{
|
|
506
|
+
width: dot,
|
|
507
|
+
height: dot,
|
|
508
|
+
borderRadius: dot,
|
|
509
|
+
left: inset + ratioOf(m.value) * usable - dot / 2,
|
|
510
|
+
top: (surfaceH - dot) / 2,
|
|
511
|
+
backgroundColor: inFill(m.value) ? alpha(onTint, 0.7) : alpha(colors.mutedForeground, dark ? 0.6 : 0.45),
|
|
512
|
+
},
|
|
513
|
+
]}
|
|
514
|
+
/>
|
|
515
|
+
))
|
|
516
|
+
: null}
|
|
517
|
+
|
|
518
|
+
{/* Thumbs (and the value tooltip, which rides along with them) */}
|
|
519
|
+
{geo
|
|
520
|
+
? vals.map((v, i) => (
|
|
521
|
+
<Animated.View
|
|
522
|
+
key={i}
|
|
523
|
+
accessible={range}
|
|
524
|
+
accessibilityRole={range ? 'adjustable' : undefined}
|
|
525
|
+
accessibilityLabel={range ? `${accessibilityLabel ?? 'Range'} ${i === 0 ? 'minimum' : 'maximum'}` : undefined}
|
|
526
|
+
accessibilityValue={range ? { min, max, now: v, text: fmt(v) } : undefined}
|
|
527
|
+
accessibilityActions={range ? [{ name: 'increment' }, { name: 'decrement' }] : undefined}
|
|
528
|
+
onAccessibilityAction={(e: AccessibilityActionEvent) => stepBy(i, e.nativeEvent.actionName === 'increment' ? 1 : -1)}
|
|
529
|
+
style={[styles.slot, { width: slotW, height: surfaceH, zIndex: active === i ? 2 : 1, transform: geo.slot[i] }]}
|
|
530
|
+
>
|
|
531
|
+
{!isBar ? (
|
|
532
|
+
<>
|
|
533
|
+
<Animated.View
|
|
534
|
+
style={[
|
|
535
|
+
styles.halo,
|
|
536
|
+
{
|
|
537
|
+
top: (surfaceH - T) / 2,
|
|
538
|
+
width: T,
|
|
539
|
+
height: T,
|
|
540
|
+
borderRadius: radius.capsule(T),
|
|
541
|
+
backgroundColor: alpha(tint, 0.16),
|
|
542
|
+
opacity: press[i],
|
|
543
|
+
transform: anim.halo[i],
|
|
544
|
+
},
|
|
545
|
+
]}
|
|
546
|
+
/>
|
|
547
|
+
<Animated.View
|
|
548
|
+
style={[
|
|
549
|
+
styles.thumb,
|
|
550
|
+
{
|
|
551
|
+
width: T,
|
|
552
|
+
height: T,
|
|
553
|
+
borderRadius: radius.capsule(T),
|
|
554
|
+
backgroundColor: thumbColor,
|
|
555
|
+
borderColor: dark ? 'transparent' : colors.border,
|
|
556
|
+
shadowOpacity: dark ? 0.5 : 0.2,
|
|
557
|
+
transform: anim.thumb[i],
|
|
558
|
+
},
|
|
559
|
+
thumbStyle,
|
|
560
|
+
]}
|
|
561
|
+
/>
|
|
562
|
+
</>
|
|
563
|
+
) : null}
|
|
564
|
+
{showValue !== 'never' && !(isBar && showValue === 'always') ? (
|
|
565
|
+
<Animated.View
|
|
566
|
+
style={[
|
|
567
|
+
styles.bubbleWrap,
|
|
568
|
+
{ bottom: bubbleBottom, left: slotW / 2 - BUBBLE_W / 2, opacity: bubble[i], transform: anim.bubble[i] },
|
|
569
|
+
]}
|
|
570
|
+
>
|
|
571
|
+
<View style={[styles.bubble, { backgroundColor: colors.foreground, borderRadius: radius.capsule(BUBBLE_H) }]}>
|
|
572
|
+
<Text numberOfLines={1} selectable={false} style={[styles.bubbleText, { color: colors.background, fontSize: sz.font }, labelStyle]}>
|
|
573
|
+
{fmt(v)}
|
|
574
|
+
</Text>
|
|
575
|
+
</View>
|
|
576
|
+
</Animated.View>
|
|
577
|
+
) : null}
|
|
578
|
+
</Animated.View>
|
|
579
|
+
))
|
|
580
|
+
: null}
|
|
581
|
+
</Animated.View>
|
|
582
|
+
</View>
|
|
583
|
+
|
|
584
|
+
{hasMarkLabels || minMax ? (
|
|
585
|
+
<View pointerEvents="none" style={[styles.labels, { height: sz.font + 8 }]}>
|
|
586
|
+
{hasMarkLabels
|
|
587
|
+
? markList.map((m) => {
|
|
588
|
+
if (!m.label || !w) return null;
|
|
589
|
+
const r = ratioOf(m.value);
|
|
590
|
+
const place: TextStyle =
|
|
591
|
+
r <= 0 ? { left: 0, textAlign: 'left' } : r >= 1 ? { right: 0, textAlign: 'right' } : { left: inset + r * usable - 40 };
|
|
592
|
+
return labelText(String(m.value), m.label, inFill(m.value), place);
|
|
593
|
+
})
|
|
594
|
+
: [
|
|
595
|
+
labelText('min', minLabel ?? fmt(min), false, { left: 0, textAlign: 'left' }),
|
|
596
|
+
labelText('max', maxLabel ?? fmt(max), false, { right: 0, textAlign: 'right' }),
|
|
597
|
+
]}
|
|
598
|
+
</View>
|
|
599
|
+
) : null}
|
|
600
|
+
</View>
|
|
601
|
+
{sideIcon(rightIcon, 1)}
|
|
602
|
+
</View>
|
|
603
|
+
</View>
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Small stroke icons for `leftIcon` / `rightIcon` / `icon`. */
|
|
608
|
+
export function SliderIcon({ name, size = 20, color }: { name: SliderIconName; size?: number; color: string }) {
|
|
609
|
+
const p = { stroke: color, strokeWidth: 1.8, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const, fill: 'none' };
|
|
610
|
+
const speaker = <Path d="M11 5 6.5 9H3.5v6h3l4.5 4V5z" {...p} />;
|
|
611
|
+
const rays = (r1: number, r2: number) =>
|
|
612
|
+
Array.from({ length: 8 }, (_, i) => {
|
|
613
|
+
const a = (i * Math.PI) / 4;
|
|
614
|
+
const c = Math.cos(a);
|
|
615
|
+
const sn = Math.sin(a);
|
|
616
|
+
return `M${(12 + c * r1).toFixed(2)} ${(12 + sn * r1).toFixed(2)}L${(12 + c * r2).toFixed(2)} ${(12 + sn * r2).toFixed(2)}`;
|
|
617
|
+
}).join('');
|
|
618
|
+
let body: React.ReactNode;
|
|
619
|
+
switch (name) {
|
|
620
|
+
case 'volume-off':
|
|
621
|
+
body = (
|
|
622
|
+
<>
|
|
623
|
+
{speaker}
|
|
624
|
+
<Path d="m16 9.5 5 5M21 9.5l-5 5" {...p} />
|
|
625
|
+
</>
|
|
626
|
+
);
|
|
627
|
+
break;
|
|
628
|
+
case 'volume-low':
|
|
629
|
+
body = (
|
|
630
|
+
<>
|
|
631
|
+
{speaker}
|
|
632
|
+
<Path d="M15.5 9a4.2 4.2 0 0 1 0 6" {...p} />
|
|
633
|
+
</>
|
|
634
|
+
);
|
|
635
|
+
break;
|
|
636
|
+
case 'volume-high':
|
|
637
|
+
body = (
|
|
638
|
+
<>
|
|
639
|
+
{speaker}
|
|
640
|
+
<Path d="M15.5 9a4.2 4.2 0 0 1 0 6M18.5 6a8.5 8.5 0 0 1 0 12" {...p} />
|
|
641
|
+
</>
|
|
642
|
+
);
|
|
643
|
+
break;
|
|
644
|
+
case 'sun-low':
|
|
645
|
+
body = (
|
|
646
|
+
<>
|
|
647
|
+
<Circle cx={12} cy={12} r={3} {...p} />
|
|
648
|
+
<Path d={rays(5.6, 6.8)} {...p} />
|
|
649
|
+
</>
|
|
650
|
+
);
|
|
651
|
+
break;
|
|
652
|
+
case 'sun-high':
|
|
653
|
+
body = (
|
|
654
|
+
<>
|
|
655
|
+
<Circle cx={12} cy={12} r={4} {...p} />
|
|
656
|
+
<Path d={rays(6.8, 9.4)} {...p} />
|
|
657
|
+
</>
|
|
658
|
+
);
|
|
659
|
+
break;
|
|
660
|
+
case 'minus':
|
|
661
|
+
body = <Path d="M6 12h12" {...p} />;
|
|
662
|
+
break;
|
|
663
|
+
case 'plus':
|
|
664
|
+
body = <Path d="M12 6v12M6 12h12" {...p} />;
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
return (
|
|
668
|
+
<Svg width={size} height={size} viewBox="0 0 24 24">
|
|
669
|
+
{body}
|
|
670
|
+
</Svg>
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const styles = StyleSheet.create({
|
|
675
|
+
root: { alignSelf: 'stretch' },
|
|
676
|
+
row: { flexDirection: 'row', alignItems: 'flex-start', gap: 12 },
|
|
677
|
+
middle: { flex: 1 },
|
|
678
|
+
iconSlot: { minWidth: 24, alignItems: 'center', justifyContent: 'center' },
|
|
679
|
+
histogram: { flexDirection: 'row', alignItems: 'flex-end', gap: 2, marginBottom: 8 },
|
|
680
|
+
histBar: { flex: 1 },
|
|
681
|
+
surface: { justifyContent: 'center' },
|
|
682
|
+
pointer: { cursor: 'pointer' },
|
|
683
|
+
layer: { ...StyleSheet.absoluteFill },
|
|
684
|
+
track: { position: 'absolute', left: 0, right: 0, overflow: 'hidden' },
|
|
685
|
+
clip: { position: 'absolute', left: 0, overflow: 'hidden' },
|
|
686
|
+
fill: { position: 'absolute', left: 0, top: 0, bottom: 0, overflow: 'hidden' },
|
|
687
|
+
counter: { position: 'absolute', left: 0, top: 0, bottom: 0 },
|
|
688
|
+
barContent: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
689
|
+
barValue: { fontWeight: '400', fontVariant: ['tabular-nums'] },
|
|
690
|
+
tick: { position: 'absolute' },
|
|
691
|
+
slot: { position: 'absolute', left: 0, top: 0, alignItems: 'center', justifyContent: 'center' },
|
|
692
|
+
halo: { position: 'absolute', left: 0 },
|
|
693
|
+
thumb: {
|
|
694
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
695
|
+
shadowColor: '#000',
|
|
696
|
+
shadowRadius: 4,
|
|
697
|
+
shadowOffset: { width: 0, height: 2 },
|
|
698
|
+
elevation: 3,
|
|
699
|
+
},
|
|
700
|
+
bubbleWrap: { position: 'absolute', width: BUBBLE_W, alignItems: 'center', transformOrigin: 'bottom' },
|
|
701
|
+
bubble: {
|
|
702
|
+
height: BUBBLE_H,
|
|
703
|
+
paddingHorizontal: 10,
|
|
704
|
+
minWidth: 36,
|
|
705
|
+
alignItems: 'center',
|
|
706
|
+
justifyContent: 'center',
|
|
707
|
+
shadowColor: '#000',
|
|
708
|
+
shadowOpacity: 0.18,
|
|
709
|
+
shadowRadius: 8,
|
|
710
|
+
shadowOffset: { width: 0, height: 3 },
|
|
711
|
+
elevation: 4,
|
|
712
|
+
},
|
|
713
|
+
bubbleText: { fontWeight: '400', fontVariant: ['tabular-nums'] },
|
|
714
|
+
labels: { marginTop: 2 },
|
|
715
|
+
markLabel: { position: 'absolute', width: 80, textAlign: 'center', fontWeight: '400', fontVariant: ['tabular-nums'] },
|
|
716
|
+
});
|