related-ui-components 3.2.8 → 3.3.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/package.json CHANGED
@@ -1,81 +1,81 @@
1
- {
2
- "name": "related-ui-components",
3
- "version": "3.2.8",
4
- "main": "./src/index.ts",
5
- "scripts": {
6
- "start": "expo start",
7
- "android": "expo start --android",
8
- "ios": "expo start --ios",
9
- "web": "expo start --web",
10
- "prepare": "bob build"
11
- },
12
- "peerDependencies": {
13
- "@expo/vector-icons": ">=14.1.0",
14
- "@gorhom/bottom-sheet": ">=5.1.6",
15
- "@ptomasroos/react-native-multi-slider": ">=2.2.2",
16
- "@react-native-community/slider": ">=4.5.6",
17
- "@react-native-picker/picker": ">=2.11.1",
18
- "@shopify/react-native-skia": ">=v2.0.0-next.4",
19
- "country-telephone-data": ">=0.6.3",
20
- "date-fns": ">=4.1.0",
21
- "expo": "53.0.20",
22
- "expo-blur": ">=14.1.5",
23
- "expo-checkbox": ">=4.1.4",
24
- "expo-clipboard": ">=7.1.4",
25
- "expo-image": ">=2.4.0",
26
- "expo-linear-gradient": ">=14.1.5",
27
- "expo-status-bar": ">=2.2.3",
28
- "react": "19.0.0",
29
- "react-native": "0.79.5",
30
- "react-native-calendars": ">=1.1312.0",
31
- "react-native-gesture-handler": ">=2.24.0",
32
- "react-native-picker-select": ">=9.3.1",
33
- "react-native-qrcode-svg": ">=6.3.15",
34
- "react-native-safe-area-context": ">=5.4.0",
35
- "react-native-svg": ">=15.11.2",
36
- "react-native-color-matrix-image-filters": ">=7.0.2",
37
- "react-native-toast-message": ">=2.3.3",
38
- "react-native-reanimated": ">=3.0.0"
39
- },
40
- "devDependencies": {
41
- "@babel/core": "^7.25.2",
42
- "@types/country-telephone-data": "^0.6.3",
43
- "@types/react": "~19.0.10",
44
- "react-native-builder-bob": "^0.40.11",
45
- "typescript": "~5.8.3"
46
- },
47
- "private": false,
48
- "exports": {
49
- ".": {
50
- "source": "./src\\index.ts",
51
- "types": "./lib\\typescript\\src\\index.d.ts",
52
- "default": "./lib\\module\\index.js"
53
- },
54
- "./package.json": "./package.json"
55
- },
56
- "types": "./lib\\typescript\\src\\index.d.ts",
57
- "files": [
58
- "src",
59
- "lib",
60
- "!**/__tests__",
61
- "!**/__fixtures__",
62
- "!**/__mocks__"
63
- ],
64
- "react-native-builder-bob": {
65
- "source": "src",
66
- "output": "lib",
67
- "targets": [
68
- [
69
- "module",
70
- {
71
- "esm": true
72
- }
73
- ],
74
- "typescript"
75
- ]
76
- },
77
- "eslintIgnore": [
78
- "node_modules/",
79
- "lib/"
80
- ]
81
- }
1
+ {
2
+ "name": "related-ui-components",
3
+ "version": "3.3.0",
4
+ "main": "lib/module/index.js",
5
+ "scripts": {
6
+ "start": "expo start",
7
+ "android": "expo start --android",
8
+ "ios": "expo start --ios",
9
+ "web": "expo start --web",
10
+ "prepare": "bob build"
11
+ },
12
+ "peerDependencies": {
13
+ "@expo/vector-icons": ">=14.1.0",
14
+ "@gorhom/bottom-sheet": ">=5.1.6",
15
+ "@ptomasroos/react-native-multi-slider": ">=2.2.2",
16
+ "@react-native-community/slider": ">=4.5.6",
17
+ "@react-native-picker/picker": ">=2.11.1",
18
+ "@shopify/react-native-skia": ">=v2.0.0-next.4",
19
+ "country-telephone-data": ">=0.6.3",
20
+ "date-fns": ">=4.1.0",
21
+ "expo": "53.0.20",
22
+ "expo-blur": ">=14.1.5",
23
+ "expo-checkbox": ">=4.1.4",
24
+ "expo-clipboard": ">=7.1.4",
25
+ "expo-image": ">=2.4.0",
26
+ "expo-linear-gradient": ">=14.1.5",
27
+ "expo-status-bar": ">=2.2.3",
28
+ "react": "19.0.0",
29
+ "react-native": "0.79.5",
30
+ "react-native-calendars": ">=1.1312.0",
31
+ "react-native-gesture-handler": ">=2.24.0",
32
+ "react-native-picker-select": ">=9.3.1",
33
+ "react-native-qrcode-svg": ">=6.3.15",
34
+ "react-native-safe-area-context": ">=5.4.0",
35
+ "react-native-svg": ">=15.11.2",
36
+ "react-native-color-matrix-image-filters": ">=7.0.2",
37
+ "react-native-toast-message": ">=2.3.3",
38
+ "react-native-reanimated": ">=3.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@babel/core": "^7.25.2",
42
+ "@types/country-telephone-data": "^0.6.3",
43
+ "@types/react": "~19.0.10",
44
+ "react-native-builder-bob": "^0.40.11",
45
+ "typescript": "~5.8.3"
46
+ },
47
+ "private": false,
48
+ "exports": {
49
+ ".": {
50
+ "source": "./src\\index.ts",
51
+ "types": "./lib\\typescript\\src\\index.d.ts",
52
+ "default": "./lib\\module\\index.js"
53
+ },
54
+ "./package.json": "./package.json"
55
+ },
56
+ "types": "./lib\\typescript\\src\\index.d.ts",
57
+ "files": [
58
+ "src",
59
+ "lib",
60
+ "!**/__tests__",
61
+ "!**/__fixtures__",
62
+ "!**/__mocks__"
63
+ ],
64
+ "react-native-builder-bob": {
65
+ "source": "src",
66
+ "output": "lib",
67
+ "targets": [
68
+ [
69
+ "module",
70
+ {
71
+ "esm": true
72
+ }
73
+ ],
74
+ "typescript"
75
+ ]
76
+ },
77
+ "eslintIgnore": [
78
+ "node_modules/",
79
+ "lib/"
80
+ ]
81
+ }
@@ -15,7 +15,6 @@ import Card from "../Card";
15
15
  import { useTheme } from "../../../theme/ThemeContext";
16
16
  import { ThemeType } from "../../../theme";
17
17
  import { LockOverlay } from "../../LockOverlay";
18
- import { BlurView } from "expo-blur";
19
18
  import { ThemedText as Text } from "../../ui";
20
19
 
21
20
  interface SelaDealCardProps {
@@ -219,16 +218,6 @@ const SelaDealCard: React.FC<SelaDealCardProps> = ({
219
218
  <Text style={descriptionTextStyle}>{description}</Text>
220
219
  )}
221
220
  </View>
222
- {price && priceContainerBlur && (
223
- <BlurView
224
- experimentalBlurMethod="dimezisBlurView"
225
- intensity={priceContainerBlur}
226
- tint="dark"
227
- style={finalPriceContainerStyle}
228
- >
229
- <Text style={finalPriceStyle}>{price}</Text>
230
- </BlurView>
231
- )}
232
221
  {price && !priceContainerBlur && (
233
222
  <View
234
223
  style={[
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useState } from "react";
1
+ import React, { useCallback, useMemo, useState } from "react";
2
2
  import { I18nManager, StyleSheet, View } from "react-native";
3
3
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
4
  import Animated, {
@@ -13,6 +13,8 @@ const THUMB_SIZE = 28;
13
13
  const RAIL_HEIGHT = 6;
14
14
  const LABEL_HEIGHT = 26;
15
15
 
16
+ type ScaleType = "linear" | "logarithmic" | "percentile";
17
+
16
18
  interface RangeSliderProps {
17
19
  min: number;
18
20
  max: number;
@@ -21,8 +23,60 @@ interface RangeSliderProps {
21
23
  initialMinValue: number;
22
24
  initialMaxValue: number;
23
25
  onValueChange: (values: { min: number; max: number }) => void;
26
+ theme?: ThemeType;
27
+ scale?: ScaleType;
28
+ /**
29
+ * For percentile scale: array of all values in your dataset
30
+ * The slider will distribute positions based on data density
31
+ */
32
+ dataPoints?: number[];
33
+ }
34
+
35
+ /**
36
+ * Creates percentile breakpoints from data
37
+ * Returns sorted unique values that represent the distribution
38
+ */
39
+ function createPercentileBreakpoints(data: number[]): number[] {
40
+ const sorted = [...new Set(data)].sort((a, b) => a - b);
41
+
42
+ // If small dataset, use all unique values
43
+ if (sorted.length <= 100) {
44
+ return sorted;
45
+ }
46
+
47
+ // For larger datasets, sample at percentile intervals
48
+ const breakpoints: number[] = [];
49
+ const numBreakpoints = 100;
50
+
51
+ for (let i = 0; i <= numBreakpoints; i++) {
52
+ const index = Math.floor((i / numBreakpoints) * (sorted.length - 1));
53
+ const value = sorted[index];
54
+ if (breakpoints[breakpoints.length - 1] !== value) {
55
+ breakpoints.push(value);
56
+ }
57
+ }
58
+
59
+ return breakpoints;
60
+ }
61
+
62
+ /**
63
+ * Binary search to find position in breakpoints
64
+ */
65
+ function findBreakpointIndex(breakpoints: number[], value: number): number {
66
+ "worklet";
67
+ let left = 0;
68
+ let right = breakpoints.length - 1;
24
69
 
25
- theme?: ThemeType
70
+ while (left < right) {
71
+ const mid = Math.floor((left + right) / 2);
72
+ if (breakpoints[mid] < value) {
73
+ left = mid + 1;
74
+ } else {
75
+ right = mid;
76
+ }
77
+ }
78
+
79
+ return left;
26
80
  }
27
81
 
28
82
  const RangeSlider: React.FC<RangeSliderProps> = ({
@@ -33,45 +87,101 @@ const RangeSlider: React.FC<RangeSliderProps> = ({
33
87
  initialMinValue,
34
88
  initialMaxValue,
35
89
  onValueChange,
36
- theme
90
+ theme,
91
+ scale = "linear",
92
+ dataPoints,
37
93
  }) => {
38
- const { theme: defaultTheme} = useTheme();
39
-
94
+ const { theme: defaultTheme } = useTheme();
40
95
  const currTheme = theme || defaultTheme;
41
-
42
96
  const styles = getStyles(currTheme);
43
-
44
97
  const isRTL = I18nManager.isRTL;
45
98
 
46
- // State for label text values, passed down to the SliderLabels component
47
99
  const [leftLabel, setLeftLabel] = useState(initialMinValue.toLocaleString());
48
100
  const [rightLabel, setRightLabel] = useState(
49
101
  initialMaxValue.toLocaleString()
50
102
  );
51
103
 
104
+ // Pre-compute breakpoints for percentile scale
105
+ const breakpoints = useMemo(() => {
106
+ if (scale === "percentile" && dataPoints?.length) {
107
+ return createPercentileBreakpoints(dataPoints);
108
+ }
109
+ return [];
110
+ }, [scale, dataPoints]);
111
+
112
+ const safeMin = scale === "logarithmic" ? Math.max(min, 0.001) : min;
113
+
52
114
  const valueToPosition = useCallback(
53
115
  (value: number) => {
54
116
  "worklet";
55
- // 3. Invert position calculation for RTL
56
- const percentage = (value - min) / (max - min);
57
- return isRTL
58
- ? (1 - percentage) * sliderWidth
59
- : percentage * sliderWidth;
117
+ let percentage: number;
118
+
119
+ if (scale === "percentile" && breakpoints.length > 1) {
120
+ // Find where this value falls in the breakpoints
121
+ const index = findBreakpointIndex(breakpoints, value);
122
+
123
+ if (index === 0) {
124
+ percentage = 0;
125
+ } else if (index >= breakpoints.length - 1) {
126
+ percentage = 1;
127
+ } else {
128
+ // Interpolate between breakpoints
129
+ const lowerVal = breakpoints[index - 1];
130
+ const upperVal = breakpoints[index];
131
+ const lowerPct = (index - 1) / (breakpoints.length - 1);
132
+ const upperPct = index / (breakpoints.length - 1);
133
+
134
+ if (upperVal === lowerVal) {
135
+ percentage = lowerPct;
136
+ } else {
137
+ const ratio = (value - lowerVal) / (upperVal - lowerVal);
138
+ percentage = lowerPct + ratio * (upperPct - lowerPct);
139
+ }
140
+ }
141
+ } else if (scale === "logarithmic") {
142
+ const logMin = Math.log(safeMin);
143
+ const logMax = Math.log(max);
144
+ const logValue = Math.log(Math.max(value, safeMin));
145
+ percentage = (logValue - logMin) / (logMax - logMin);
146
+ } else {
147
+ percentage = (value - min) / (max - min);
148
+ }
149
+
150
+ return isRTL ? (1 - percentage) * sliderWidth : percentage * sliderWidth;
60
151
  },
61
- [min, max, sliderWidth, isRTL]
152
+ [min, max, safeMin, sliderWidth, isRTL, scale, breakpoints]
62
153
  );
63
154
 
64
155
  const positionToValue = useCallback(
65
156
  (position: number) => {
66
157
  "worklet";
67
- // 4. Invert value calculation for RTL
68
- const percentage = position / sliderWidth;
69
- const rawValue = isRTL
70
- ? (1 - percentage) * (max - min) + min
71
- : percentage * (max - min) + min;
158
+ const percentage = isRTL
159
+ ? 1 - position / sliderWidth
160
+ : position / sliderWidth;
161
+
162
+ let rawValue: number;
163
+
164
+ if (scale === "percentile" && breakpoints.length > 1) {
165
+ // Map percentage to breakpoint index
166
+ const exactIndex = percentage * (breakpoints.length - 1);
167
+ const lowerIndex = Math.floor(exactIndex);
168
+ const upperIndex = Math.min(lowerIndex + 1, breakpoints.length - 1);
169
+ const ratio = exactIndex - lowerIndex;
170
+
171
+ rawValue =
172
+ breakpoints[lowerIndex] +
173
+ ratio * (breakpoints[upperIndex] - breakpoints[lowerIndex]);
174
+ } else if (scale === "logarithmic") {
175
+ const logMin = Math.log(safeMin);
176
+ const logMax = Math.log(max);
177
+ rawValue = Math.exp(logMin + percentage * (logMax - logMin));
178
+ } else {
179
+ rawValue = percentage * (max - min) + min;
180
+ }
181
+
72
182
  return Math.round(rawValue / step) * step;
73
183
  },
74
- [min, max, step, sliderWidth, isRTL]
184
+ [min, max, safeMin, step, sliderWidth, isRTL, scale, breakpoints]
75
185
  );
76
186
 
77
187
  const leftPosition = useSharedValue(valueToPosition(initialMinValue));
@@ -95,23 +205,20 @@ const RangeSlider: React.FC<RangeSliderProps> = ({
95
205
  if (activeThumb.value === null) return;
96
206
  const newPos = context.value.x + e.translationX;
97
207
 
98
- // 5. Adjust clamping logic for RTL
99
208
  if (activeThumb.value === "left") {
100
209
  const lowerBound = isRTL ? rightPosition.value + THUMB_SIZE : 0;
101
- const upperBound = isRTL ? sliderWidth : rightPosition.value - THUMB_SIZE;
102
- const clampedPos = Math.max(
103
- Math.min(newPos, upperBound),
104
- lowerBound
105
- );
210
+ const upperBound = isRTL
211
+ ? sliderWidth
212
+ : rightPosition.value - THUMB_SIZE;
213
+ const clampedPos = Math.max(Math.min(newPos, upperBound), lowerBound);
106
214
  leftPosition.value = clampedPos;
107
215
  runOnJS(setLeftLabel)(positionToValue(clampedPos).toLocaleString());
108
216
  } else {
109
217
  const lowerBound = isRTL ? 0 : leftPosition.value + THUMB_SIZE;
110
- const upperBound = isRTL ? leftPosition.value - THUMB_SIZE : sliderWidth;
111
- const clampedPos = Math.max(
112
- Math.min(newPos, upperBound),
113
- lowerBound
114
- );
218
+ const upperBound = isRTL
219
+ ? leftPosition.value - THUMB_SIZE
220
+ : sliderWidth;
221
+ const clampedPos = Math.max(Math.min(newPos, upperBound), lowerBound);
115
222
  rightPosition.value = clampedPos;
116
223
  runOnJS(setRightLabel)(positionToValue(clampedPos).toLocaleString());
117
224
  }
@@ -124,9 +231,8 @@ const RangeSlider: React.FC<RangeSliderProps> = ({
124
231
  activeThumb.value = null;
125
232
  });
126
233
 
127
-
128
234
  const animatedLeftThumbStyle = useAnimatedStyle(() => ({
129
- transform: [{ translateX: leftPosition.value - (isRTL ? THUMB_SIZE : 0)}],
235
+ transform: [{ translateX: leftPosition.value - (isRTL ? THUMB_SIZE : 0) }],
130
236
  }));
131
237
 
132
238
  const animatedRightThumbStyle = useAnimatedStyle(() => ({
@@ -134,7 +240,6 @@ const RangeSlider: React.FC<RangeSliderProps> = ({
134
240
  }));
135
241
 
136
242
  const animatedActiveRailStyle = useAnimatedStyle(() => {
137
- // 6. Use Math.min and Math.max to correctly draw the active rail in both LTR and RTL
138
243
  const start = Math.min(leftPosition.value, rightPosition.value);
139
244
  const end = Math.max(leftPosition.value, rightPosition.value);
140
245
  return {
@@ -182,7 +287,7 @@ const getStyles = (theme: ThemeType) =>
182
287
  height: LABEL_HEIGHT + THUMB_SIZE,
183
288
  justifyContent: "center",
184
289
  marginTop: LABEL_HEIGHT,
185
- direction: "ltr"
290
+ direction: "ltr",
186
291
  },
187
292
  railContainer: {
188
293
  justifyContent: "center",
@@ -215,4 +320,4 @@ const getStyles = (theme: ThemeType) =>
215
320
  },
216
321
  });
217
322
 
218
- export default RangeSlider;
323
+ export default RangeSlider;