| 1 | import React from 'react';
|
| 2 | import {
|
| 3 | View,
|
| 4 | StyleSheet,
|
| 5 | ViewStyle,
|
| 6 | } from 'react-native';
|
| 7 | import { Text } from '@/components/ui/Text';
|
| 8 | import Slider from '@react-native-community/slider';
|
| 9 | import { useThemeColor } from '@/hooks/use-theme-color';
|
| 10 | import { Theme } from '@/constants/theme';
|
| 11 |
|
| 12 | type SliderVariant = 'rich' | 'compact';
|
| 13 |
|
| 14 | interface ScaleSliderProps {
|
| 15 | label: string;
|
| 16 | value: number;
|
| 17 | onValueChange: (value: number) => void;
|
| 18 | onValueFormat?: (value: number) => string;
|
| 19 | min?: number;
|
| 20 | max?: number;
|
| 21 | step?: number;
|
| 22 | minLabel?: string;
|
| 23 | maxLabel?: string;
|
| 24 | icon?: React.ReactNode; // rich variant only
|
| 25 | showMax?: boolean; // shows "x/10" format instead of just "x"
|
| 26 | variant?: SliderVariant;
|
| 27 | style?: ViewStyle;
|
| 28 | }
|
| 29 |
|
| 30 | export const ScaleSlider: React.FC<ScaleSliderProps> = ({
|
| 31 | label,
|
| 32 | value,
|
| 33 | onValueChange,
|
| 34 | onValueFormat,
|
| 35 | min = 0,
|
| 36 | max = 10,
|
| 37 | step = 1,
|
| 38 | minLabel,
|
| 39 | maxLabel,
|
| 40 | icon,
|
| 41 | showMax,
|
| 42 | variant = 'compact',
|
| 43 | style,
|
| 44 | }) => {
|
| 45 | const textColor = useThemeColor({}, 'text');
|
| 46 | const tintColor = useThemeColor({}, 'tint');
|
| 47 | const trackingColor = useThemeColor({}, 'sliderTracking');
|
| 48 | const sliderLabelsColor = useThemeColor({}, 'sliderLabels');
|
| 49 |
|
| 50 | const isRich = variant === 'rich';
|
| 51 |
|
| 52 | const valueDisplay = showMax || !isRich
|
| 53 | ? `${value}/${max}`
|
| 54 | : `${value}`;
|
| 55 |
|
| 56 | return (
|
| 57 | <View style={[styles.container, style]}>
|
| 58 |
|
| 59 | {/* Header row */}
|
| 60 | <View style={[styles.header, isRich ? styles.headerRich : styles.headerCompact ]}>
|
| 61 | <View style={styles.labelRow}>
|
| 62 | {isRich && icon && (
|
| 63 | <View style={styles.iconWrapper}>{icon}</View>
|
| 64 | )}
|
| 65 | <Text style={[
|
| 66 | isRich ? styles.labelRich : styles.labelCompact,
|
| 67 | { color: textColor },
|
| 68 | ]}>
|
| 69 | {label}
|
| 70 | </Text>
|
| 71 | </View>
|
| 72 |
|
| 73 | <Text style={[
|
| 74 | isRich ? styles.valueRich : styles.valueCompact,
|
| 75 | { color: textColor },
|
| 76 | ]}>
|
| 77 | {(onValueFormat ? onValueFormat(value) : valueDisplay)}
|
| 78 | </Text>
|
| 79 | </View>
|
| 80 |
|
| 81 | {/* XXX: Rich variant needs some extra margin to account for icons */}
|
| 82 | <View style={{ marginRight: isRich ? -10 : -15, marginLeft: isRich ? -10 : -15 }}>
|
| 83 | {/* Slider has a glitch with the full padding of it */}
|
| 84 | <Slider
|
| 85 | style={styles.slider}
|
| 86 | minimumValue={min}
|
| 87 | maximumValue={max}
|
| 88 | step={step}
|
| 89 | value={value}
|
| 90 | onValueChange={onValueChange}
|
| 91 | minimumTrackTintColor={trackingColor}
|
| 92 | maximumTrackTintColor={trackingColor}
|
| 93 | thumbTintColor={tintColor}
|
| 94 | />
|
| 95 | </View>
|
| 96 |
|
| 97 | {/* Min / Max labels */}
|
| 98 | {(minLabel || maxLabel) && (
|
| 99 | <View style={styles.rangeLabels}>
|
| 100 | {minLabel && (
|
| 101 | <Text style={[styles.rangeLabel, { color: sliderLabelsColor }]}>
|
| 102 | {minLabel.toUpperCase()}
|
| 103 | </Text>
|
| 104 | )}
|
| 105 | {maxLabel && (
|
| 106 | <Text style={[styles.rangeLabel, { color: sliderLabelsColor }]}>
|
| 107 | {maxLabel.toUpperCase()}
|
| 108 | </Text>
|
| 109 | )}
|
| 110 | </View>
|
| 111 | )}
|
| 112 |
|
| 113 | </View>
|
| 114 | );
|
| 115 | };
|
| 116 |
|
| 117 | const styles = StyleSheet.create({
|
| 118 | container: {
|
| 119 | width: '100%',
|
| 120 | },
|
| 121 | header: {
|
| 122 | flexDirection: 'row',
|
| 123 | justifyContent: 'space-between',
|
| 124 | alignItems: 'center',
|
| 125 | },
|
| 126 |
|
| 127 | // Rich has bigger margin than compact
|
| 128 | headerRich: {
|
| 129 | marginBottom: 4,
|
| 130 | },
|
| 131 |
|
| 132 | headerCompact: {
|
| 133 | marginBottom: 1
|
| 134 | },
|
| 135 |
|
| 136 | labelRow: {
|
| 137 | flexDirection: 'row',
|
| 138 | alignItems: 'center',
|
| 139 | gap: 8,
|
| 140 | },
|
| 141 | iconWrapper: {
|
| 142 | justifyContent: 'center',
|
| 143 | alignItems: 'center',
|
| 144 | },
|
| 145 |
|
| 146 | // Rich variant
|
| 147 | labelRich: {
|
| 148 | fontSize: 14,
|
| 149 | fontWeight: 500
|
| 150 | },
|
| 151 | valueRich: {
|
| 152 | fontSize: 18,
|
| 153 | lineHeight: 28,
|
| 154 | fontWeight: 600
|
| 155 | },
|
| 156 |
|
| 157 | // Compact variant
|
| 158 | labelCompact: {
|
| 159 | fontSize: 12,
|
| 160 | fontWeight: 600,
|
| 161 | lineHeight: 16
|
| 162 | },
|
| 163 | valueCompact: {
|
| 164 | fontSize: 12,
|
| 165 | lineHeight: 16,
|
| 166 | fontWeight: 600,
|
| 167 | color: Theme.colors.light.textSecondary,
|
| 168 | },
|
| 169 |
|
| 170 | slider: {
|
| 171 | width: '100%',
|
| 172 | height: 40,
|
| 173 | },
|
| 174 | rangeLabels: {
|
| 175 | flexDirection: 'row',
|
| 176 | justifyContent: 'space-between',
|
| 177 | marginTop: -4,
|
| 178 | },
|
| 179 | rangeLabel: {
|
| 180 | ...Theme.typography.labelXs,
|
| 181 | fontWeight: 500,
|
| 182 | },
|
| 183 | });
|