@particle-network/ui-native 0.4.2-beta.23 → 0.4.2-beta.24

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.
@@ -0,0 +1,7 @@
1
+ export declare const THUMB_SIZE = 12;
2
+ export declare const THUMB_INNER_SIZE = 8;
3
+ export declare const TRACK_HEIGHT = 2;
4
+ export declare const MARK_SIZE = 4;
5
+ export declare const TOOLTIP_ARROW_SIZE = 6;
6
+ export declare const TOOLTIP_MARGIN_BOTTOM = 4;
7
+ export declare const LABEL_WIDTH = 40;
@@ -0,0 +1,8 @@
1
+ const THUMB_SIZE = 12;
2
+ const THUMB_INNER_SIZE = 8;
3
+ const TRACK_HEIGHT = 2;
4
+ const MARK_SIZE = 4;
5
+ const TOOLTIP_ARROW_SIZE = 6;
6
+ const TOOLTIP_MARGIN_BOTTOM = 4;
7
+ const LABEL_WIDTH = 40;
8
+ export { LABEL_WIDTH, MARK_SIZE, THUMB_INNER_SIZE, THUMB_SIZE, TOOLTIP_ARROW_SIZE, TOOLTIP_MARGIN_BOTTOM, TRACK_HEIGHT };
@@ -0,0 +1 @@
1
+ export * from './slider';
@@ -0,0 +1 @@
1
+ export * from "./slider.js";
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ import type { UXSliderProps } from './slider.types';
3
+ export declare const UXSlider: React.FC<UXSliderProps>;
@@ -0,0 +1,283 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState } from "react";
3
+ import { View } from "react-native";
4
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
5
+ import react_native_reanimated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
6
+ import { scheduleOnRN } from "react-native-worklets";
7
+ import { useTheme } from "../../hooks/index.js";
8
+ import { Box } from "../layout/index.js";
9
+ import { Text } from "../Text/index.js";
10
+ import { UXPressable } from "../UXPressable/index.js";
11
+ import { LABEL_WIDTH, MARK_SIZE, THUMB_SIZE } from "./constants.js";
12
+ import { useStyles } from "./slider.styles.js";
13
+ const UXSlider = ({ color = 'primary', minValue = 0, maxValue = 100, defaultValue, value: controlledValue, onChange, onChangeEnd, fillOffset, marks, showMarksSteps = true, step, showTooltip, getValue, ...restProps })=>{
14
+ const { colors } = useTheme();
15
+ const [trackWidth, setTrackWidth] = useState(0);
16
+ const [internalValue, setInternalValue] = useState(defaultValue ?? minValue);
17
+ const [isDragging, setIsDragging] = useState(false);
18
+ const [tooltipWidth, setTooltipWidth] = useState(0);
19
+ const styles = useStyles({
20
+ color
21
+ });
22
+ let currentValue = void 0 !== controlledValue ? Math.max(minValue, Math.min(maxValue, controlledValue)) : internalValue;
23
+ if (!Number.isFinite(currentValue)) currentValue = fillOffset ?? minValue;
24
+ const thumbX = useSharedValue(0);
25
+ const trackWidthShared = useSharedValue(0);
26
+ const valueToPosition = useCallback((val)=>{
27
+ if (0 === trackWidth) return THUMB_SIZE / 2;
28
+ const usableWidth = trackWidth - THUMB_SIZE;
29
+ return THUMB_SIZE / 2 + (val - minValue) / (maxValue - minValue) * usableWidth;
30
+ }, [
31
+ trackWidth,
32
+ minValue,
33
+ maxValue
34
+ ]);
35
+ const valueToMarkThumbPosition = useCallback((val)=>{
36
+ if (0 === trackWidth) return MARK_SIZE / 2;
37
+ const usableWidth = trackWidth - MARK_SIZE;
38
+ return MARK_SIZE / 2 + (val - minValue) / (maxValue - minValue) * usableWidth;
39
+ }, [
40
+ trackWidth,
41
+ minValue,
42
+ maxValue
43
+ ]);
44
+ const positionToValueWorklet = (pos, width)=>{
45
+ 'worklet';
46
+ if (0 === width) return minValue;
47
+ const usableWidth = width - THUMB_SIZE;
48
+ if (usableWidth <= 0) return minValue;
49
+ const adjustedPos = pos - THUMB_SIZE / 2;
50
+ const percentage = Math.max(0, Math.min(1, adjustedPos / usableWidth));
51
+ let val = minValue + percentage * (maxValue - minValue);
52
+ if (step && step > 0) {
53
+ val = Math.round(val / step) * step;
54
+ const decimals = (step.toString().split('.')[1] || '').length;
55
+ val = Number(val.toFixed(decimals));
56
+ }
57
+ return Math.max(minValue, Math.min(maxValue, val));
58
+ };
59
+ useEffect(()=>{
60
+ thumbX.value = valueToPosition(currentValue);
61
+ }, [
62
+ currentValue,
63
+ valueToPosition,
64
+ thumbX
65
+ ]);
66
+ useEffect(()=>{
67
+ trackWidthShared.value = trackWidth;
68
+ }, [
69
+ trackWidth,
70
+ trackWidthShared
71
+ ]);
72
+ const setDraggingTrue = useCallback(()=>setIsDragging(true), []);
73
+ const setDraggingFalse = useCallback(()=>setIsDragging(false), []);
74
+ const handleValueChange = useCallback((newValue)=>{
75
+ if (void 0 === controlledValue) setInternalValue(newValue);
76
+ onChange?.(newValue);
77
+ }, [
78
+ controlledValue,
79
+ onChange
80
+ ]);
81
+ const handleChangeEnd = useCallback((finalValue)=>{
82
+ onChangeEnd?.(finalValue);
83
+ }, [
84
+ onChangeEnd
85
+ ]);
86
+ const panGesture = Gesture.Pan().activeOffsetX([
87
+ -8,
88
+ 8
89
+ ]).failOffsetY([
90
+ -16,
91
+ 16
92
+ ]).onStart((event)=>{
93
+ 'worklet';
94
+ scheduleOnRN(setDraggingTrue);
95
+ const minPos = THUMB_SIZE / 2;
96
+ const maxPos = trackWidthShared.value - THUMB_SIZE / 2;
97
+ const touchX = Math.max(minPos, Math.min(event.x, maxPos));
98
+ thumbX.value = touchX;
99
+ }).onUpdate((event)=>{
100
+ 'worklet';
101
+ const minPos = THUMB_SIZE / 2;
102
+ const maxPos = trackWidthShared.value - THUMB_SIZE / 2;
103
+ const touchX = Math.max(minPos, Math.min(event.x, maxPos));
104
+ thumbX.value = touchX;
105
+ const newValue = positionToValueWorklet(touchX, trackWidthShared.value);
106
+ scheduleOnRN(handleValueChange, newValue);
107
+ }).onEnd(()=>{
108
+ 'worklet';
109
+ scheduleOnRN(setDraggingFalse);
110
+ const finalValue = positionToValueWorklet(thumbX.value, trackWidthShared.value);
111
+ scheduleOnRN(handleChangeEnd, finalValue);
112
+ });
113
+ const tapGesture = Gesture.Tap().onEnd((event)=>{
114
+ 'worklet';
115
+ if (event.y > THUMB_SIZE) return;
116
+ const minPos = THUMB_SIZE / 2;
117
+ const maxPos = trackWidthShared.value - THUMB_SIZE / 2;
118
+ const touchX = Math.max(minPos, Math.min(event.x, maxPos));
119
+ thumbX.value = touchX;
120
+ const newValue = positionToValueWorklet(touchX, trackWidthShared.value);
121
+ scheduleOnRN(handleValueChange, newValue);
122
+ scheduleOnRN(handleChangeEnd, newValue);
123
+ });
124
+ const gesture = Gesture.Simultaneous(panGesture, tapGesture);
125
+ const labelBlockerGesture = Gesture.Native();
126
+ const thumbAnimatedStyle = useAnimatedStyle(()=>({
127
+ transform: [
128
+ {
129
+ translateX: thumbX.value - THUMB_SIZE / 2
130
+ }
131
+ ]
132
+ }));
133
+ const fillAnimatedStyle = useAnimatedStyle(()=>{
134
+ if (void 0 !== fillOffset) {
135
+ const offsetPos = (fillOffset - minValue) / (maxValue - minValue) * trackWidthShared.value;
136
+ const currentPos = thumbX.value;
137
+ return currentPos >= offsetPos ? {
138
+ left: offsetPos,
139
+ width: currentPos - offsetPos
140
+ } : {
141
+ left: currentPos,
142
+ width: offsetPos - currentPos
143
+ };
144
+ }
145
+ return {
146
+ left: 0,
147
+ width: thumbX.value
148
+ };
149
+ });
150
+ const tooltipAnimatedStyle = useAnimatedStyle(()=>{
151
+ const left = thumbX.value - tooltipWidth / 2;
152
+ const clampedLeft = Math.max(0, Math.min(trackWidthShared.value - tooltipWidth, left));
153
+ return {
154
+ transform: [
155
+ {
156
+ translateX: clampedLeft
157
+ }
158
+ ]
159
+ };
160
+ });
161
+ const getTooltipText = useCallback(()=>{
162
+ if (getValue) return getValue(currentValue);
163
+ return currentValue.toString();
164
+ }, [
165
+ currentValue,
166
+ getValue
167
+ ]);
168
+ const handleTrackLayout = (event)=>setTrackWidth(event.nativeEvent.layout.width);
169
+ const handleTooltipLayout = (event)=>setTooltipWidth(event.nativeEvent.layout.width);
170
+ const handleMarkLabelPress = useCallback((markValue)=>{
171
+ setIsDragging(false);
172
+ thumbX.value = valueToPosition(markValue);
173
+ handleValueChange(markValue);
174
+ handleChangeEnd(markValue);
175
+ }, [
176
+ thumbX,
177
+ valueToPosition,
178
+ handleValueChange,
179
+ handleChangeEnd
180
+ ]);
181
+ return /*#__PURE__*/ jsx(Box, {
182
+ fullWidth: true,
183
+ ph: marks && marks.length > 0 && marks.some((mark)=>mark.value === maxValue) ? LABEL_WIDTH / 2 : 0,
184
+ ...restProps,
185
+ children: /*#__PURE__*/ jsx(GestureDetector, {
186
+ gesture: gesture,
187
+ children: /*#__PURE__*/ jsxs(react_native_reanimated.View, {
188
+ style: styles.gestureWrapper,
189
+ children: [
190
+ /*#__PURE__*/ jsxs(View, {
191
+ style: styles.trackContainer,
192
+ onLayout: handleTrackLayout,
193
+ children: [
194
+ showTooltip && isDragging && /*#__PURE__*/ jsx(react_native_reanimated.View, {
195
+ style: [
196
+ styles.tooltipWrapper,
197
+ tooltipAnimatedStyle
198
+ ],
199
+ pointerEvents: "none",
200
+ onLayout: handleTooltipLayout,
201
+ children: /*#__PURE__*/ jsxs(View, {
202
+ style: styles.tooltip,
203
+ children: [
204
+ /*#__PURE__*/ jsx(Text, {
205
+ children: getTooltipText()
206
+ }),
207
+ /*#__PURE__*/ jsx(View, {
208
+ style: styles.tooltipArrow
209
+ })
210
+ ]
211
+ })
212
+ }),
213
+ /*#__PURE__*/ jsx(Box, {
214
+ style: styles.track
215
+ }),
216
+ /*#__PURE__*/ jsx(react_native_reanimated.View, {
217
+ style: [
218
+ styles.fill,
219
+ fillAnimatedStyle
220
+ ]
221
+ }),
222
+ showMarksSteps && marks?.map((mark, index)=>{
223
+ const markPos = valueToMarkThumbPosition(mark.value);
224
+ const isActive = void 0 !== fillOffset ? currentValue >= fillOffset && mark.value <= currentValue && mark.value >= fillOffset || currentValue < fillOffset && mark.value >= currentValue && mark.value <= fillOffset : mark.value <= currentValue;
225
+ return /*#__PURE__*/ jsx(View, {
226
+ style: [
227
+ styles.mark,
228
+ {
229
+ left: markPos - MARK_SIZE / 2,
230
+ backgroundColor: isActive ? colors[color] : colors.tertiary
231
+ }
232
+ ]
233
+ }, index);
234
+ }),
235
+ /*#__PURE__*/ jsx(react_native_reanimated.View, {
236
+ style: [
237
+ styles.thumb,
238
+ thumbAnimatedStyle
239
+ ]
240
+ })
241
+ ]
242
+ }),
243
+ marks && marks.length > 0 && /*#__PURE__*/ jsx(GestureDetector, {
244
+ gesture: labelBlockerGesture,
245
+ children: /*#__PURE__*/ jsx(View, {
246
+ style: styles.labelsContainer,
247
+ children: marks.map((mark, index)=>{
248
+ const isActive = void 0 !== fillOffset ? currentValue >= fillOffset && mark.value <= currentValue && mark.value >= fillOffset || currentValue < fillOffset && mark.value >= currentValue && mark.value <= fillOffset : mark.value <= currentValue;
249
+ return /*#__PURE__*/ jsx(UXPressable, {
250
+ center: true,
251
+ style: [
252
+ styles.markLabelWrapper,
253
+ {
254
+ left: valueToMarkThumbPosition(mark.value) - LABEL_WIDTH / 2
255
+ }
256
+ ],
257
+ hitSlop: {
258
+ top: 8,
259
+ bottom: 8,
260
+ left: 8,
261
+ right: 8
262
+ },
263
+ onPress: ()=>handleMarkLabelPress(mark.value),
264
+ children: /*#__PURE__*/ jsx(Text, {
265
+ caption1: true,
266
+ style: [
267
+ {
268
+ color: isActive ? colors.foreground : colors.secondary
269
+ }
270
+ ],
271
+ numberOfLines: 1,
272
+ children: mark.label
273
+ })
274
+ }, index);
275
+ })
276
+ })
277
+ })
278
+ ]
279
+ })
280
+ })
281
+ });
282
+ };
283
+ export { UXSlider };
@@ -0,0 +1,92 @@
1
+ import type { UXSliderProps } from './slider.types';
2
+ export declare const useStyles: ({ color }: UXSliderProps) => {
3
+ gestureWrapper: {
4
+ width: "100%";
5
+ };
6
+ trackContainer: {
7
+ width: "100%";
8
+ justifyContent: "center";
9
+ position: "relative";
10
+ height: number;
11
+ };
12
+ track: {
13
+ width: "100%";
14
+ borderRadius: number;
15
+ position: "absolute";
16
+ backgroundColor: string;
17
+ height: number;
18
+ };
19
+ fill: {
20
+ position: "absolute";
21
+ borderRadius: number;
22
+ backgroundColor: string;
23
+ height: number;
24
+ };
25
+ mark: {
26
+ position: "absolute";
27
+ width: number;
28
+ height: number;
29
+ top: "50%";
30
+ marginTop: number;
31
+ borderRadius: number;
32
+ };
33
+ thumb: {
34
+ alignItems: "center";
35
+ justifyContent: "center";
36
+ position: "absolute";
37
+ top: "50%";
38
+ marginTop: number;
39
+ width: number;
40
+ height: number;
41
+ borderRadius: number;
42
+ shadowColor: string;
43
+ shadowOffset: {
44
+ width: number;
45
+ height: number;
46
+ };
47
+ shadowOpacity: number;
48
+ shadowRadius: number;
49
+ elevation: number;
50
+ backgroundColor: string;
51
+ };
52
+ labelsContainer: {
53
+ width: "100%";
54
+ height: number;
55
+ marginTop: number;
56
+ position: "relative";
57
+ };
58
+ markLabelWrapper: {
59
+ position: "absolute";
60
+ width: number;
61
+ overflow: "visible";
62
+ };
63
+ tooltipWrapper: {
64
+ position: "absolute";
65
+ left: number;
66
+ top: number;
67
+ justifyContent: "center";
68
+ alignItems: "center";
69
+ zIndex: number;
70
+ };
71
+ tooltip: {
72
+ position: "absolute";
73
+ paddingHorizontal: number;
74
+ paddingVertical: number;
75
+ borderRadius: number;
76
+ alignItems: "center";
77
+ backgroundColor: string;
78
+ bottom: number;
79
+ };
80
+ tooltipArrow: {
81
+ position: "absolute";
82
+ bottom: number;
83
+ width: number;
84
+ height: number;
85
+ borderLeftWidth: number;
86
+ borderRightWidth: number;
87
+ borderTopWidth: number;
88
+ borderLeftColor: string;
89
+ borderRightColor: string;
90
+ borderTopColor: string;
91
+ };
92
+ };
@@ -0,0 +1,100 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { useTheme } from "../../hooks/index.js";
3
+ import { LABEL_WIDTH, MARK_SIZE, THUMB_SIZE, TOOLTIP_ARROW_SIZE, TOOLTIP_MARGIN_BOTTOM, TRACK_HEIGHT } from "./constants.js";
4
+ const useStyles = ({ color })=>{
5
+ const { colors } = useTheme();
6
+ const colorValue = colors[color];
7
+ const styles = StyleSheet.create({
8
+ gestureWrapper: {
9
+ width: '100%'
10
+ },
11
+ trackContainer: {
12
+ width: '100%',
13
+ justifyContent: 'center',
14
+ position: 'relative',
15
+ height: THUMB_SIZE
16
+ },
17
+ track: {
18
+ width: '100%',
19
+ borderRadius: TRACK_HEIGHT / 2,
20
+ position: 'absolute',
21
+ backgroundColor: colors['bg-200'],
22
+ height: TRACK_HEIGHT
23
+ },
24
+ fill: {
25
+ position: 'absolute',
26
+ borderRadius: TRACK_HEIGHT / 2,
27
+ backgroundColor: colorValue,
28
+ height: TRACK_HEIGHT
29
+ },
30
+ mark: {
31
+ position: 'absolute',
32
+ width: MARK_SIZE,
33
+ height: MARK_SIZE,
34
+ top: '50%',
35
+ marginTop: -MARK_SIZE / 2,
36
+ borderRadius: 999
37
+ },
38
+ thumb: {
39
+ alignItems: 'center',
40
+ justifyContent: 'center',
41
+ position: 'absolute',
42
+ top: '50%',
43
+ marginTop: -THUMB_SIZE / 2,
44
+ width: THUMB_SIZE,
45
+ height: THUMB_SIZE,
46
+ borderRadius: 999,
47
+ shadowColor: '#000',
48
+ shadowOffset: {
49
+ width: 0,
50
+ height: 2
51
+ },
52
+ shadowOpacity: 0.1,
53
+ shadowRadius: 3,
54
+ elevation: 3,
55
+ backgroundColor: colorValue
56
+ },
57
+ labelsContainer: {
58
+ width: '100%',
59
+ height: 14,
60
+ marginTop: 6,
61
+ position: 'relative'
62
+ },
63
+ markLabelWrapper: {
64
+ position: 'absolute',
65
+ width: LABEL_WIDTH,
66
+ overflow: 'visible'
67
+ },
68
+ tooltipWrapper: {
69
+ position: 'absolute',
70
+ left: 0,
71
+ top: 0,
72
+ justifyContent: 'center',
73
+ alignItems: 'center',
74
+ zIndex: 100
75
+ },
76
+ tooltip: {
77
+ position: 'absolute',
78
+ paddingHorizontal: 12,
79
+ paddingVertical: 6,
80
+ borderRadius: 6,
81
+ alignItems: 'center',
82
+ backgroundColor: colorValue,
83
+ bottom: THUMB_SIZE / 2 + TOOLTIP_MARGIN_BOTTOM
84
+ },
85
+ tooltipArrow: {
86
+ position: 'absolute',
87
+ bottom: -TOOLTIP_ARROW_SIZE + 1,
88
+ width: 0,
89
+ height: 0,
90
+ borderLeftWidth: TOOLTIP_ARROW_SIZE,
91
+ borderRightWidth: TOOLTIP_ARROW_SIZE,
92
+ borderTopWidth: TOOLTIP_ARROW_SIZE,
93
+ borderLeftColor: 'transparent',
94
+ borderRightColor: 'transparent',
95
+ borderTopColor: colorValue
96
+ }
97
+ });
98
+ return styles;
99
+ };
100
+ export { useStyles };
@@ -0,0 +1,30 @@
1
+ import type { UXForegroundColor } from '@particle-network/ui-shared';
2
+ import type { BoxProps } from '../layout';
3
+ export interface UXSliderProps extends BoxProps {
4
+ color?: Exclude<UXForegroundColor, 'default' | 'white'>;
5
+ minValue?: number;
6
+ maxValue?: number;
7
+ defaultValue?: number;
8
+ value?: number;
9
+ onChange?: (value: number) => void;
10
+ onChangeEnd?: (value: number) => void;
11
+ /**
12
+ * 起始填充的偏移量
13
+ */
14
+ fillOffset?: number;
15
+ /**
16
+ * 滑杆底部标记点
17
+ */
18
+ marks?: {
19
+ value: number;
20
+ label: string;
21
+ }[];
22
+ /** 是否显示滑杆上标记记点 */
23
+ showMarksSteps?: boolean;
24
+ /** 拖动步长 */
25
+ step?: number;
26
+ /** 拖动时是否显示气泡提示框 */
27
+ showTooltip?: boolean;
28
+ /** 格式化 value,用于显示 tooltip 的文本 */
29
+ getValue?: (value: number) => string;
30
+ }
File without changes
@@ -13,6 +13,7 @@ export * from './UXListBox';
13
13
  export * from './UXModal';
14
14
  export * from './UXPressable';
15
15
  export * from './UXRadio';
16
+ export * from './UXSlider';
16
17
  export * from './UXSpinner';
17
18
  export * from './UXSwitch';
18
19
  export * from './UXTabs';
@@ -13,6 +13,7 @@ export * from "./UXListBox/index.js";
13
13
  export * from "./UXModal/index.js";
14
14
  export * from "./UXPressable/index.js";
15
15
  export * from "./UXRadio/index.js";
16
+ export * from "./UXSlider/index.js";
16
17
  export * from "./UXSpinner/index.js";
17
18
  export * from "./UXSwitch/index.js";
18
19
  export * from "./UXTabs/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-network/ui-native",
3
- "version": "0.4.2-beta.23",
3
+ "version": "0.4.2-beta.24",
4
4
  "main": "./entry.js",
5
5
  "react-native": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -89,8 +89,8 @@
89
89
  "unfetch": "^4.2.0",
90
90
  "vite": "^6.3.5",
91
91
  "zustand": "^5.0.8",
92
- "@particle-network/lintstaged-config": "0.1.0",
93
92
  "@particle-network/icons": "0.4.2-beta.9",
93
+ "@particle-network/lintstaged-config": "0.1.0",
94
94
  "@particle-network/eslint-config": "0.3.0"
95
95
  },
96
96
  "overrides": {