@umituz/react-native-photo-editor 1.1.2 → 2.0.2
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 +19 -13
- package/src/PhotoEditor.tsx +162 -44
- package/src/components/AIMagicSheet.tsx +57 -33
- package/src/components/AdjustmentsSheet.tsx +108 -0
- package/src/components/ColorPicker.tsx +77 -0
- package/src/components/DraggableSticker.tsx +92 -31
- package/src/components/DraggableText.tsx +98 -35
- package/src/components/EditorCanvas.tsx +55 -31
- package/src/components/EditorToolbar.tsx +121 -46
- package/src/components/FilterPicker.tsx +31 -24
- package/src/components/FontControls.tsx +71 -20
- package/src/components/LayerManager.tsx +115 -30
- package/src/components/Slider.tsx +112 -0
- package/src/components/StickerPicker.tsx +5 -4
- package/src/components/TextEditorSheet.tsx +116 -10
- package/src/constants.ts +63 -45
- package/src/core/HistoryManager.ts +11 -33
- package/src/hooks/useImagePicker.ts +69 -0
- package/src/hooks/usePhotoEditor.ts +73 -21
- package/src/hooks/usePhotoEditorUI.ts +100 -25
- package/src/index.ts +8 -0
- package/src/styles.ts +24 -6
- package/src/types.ts +12 -1
- package/src/utils/mediaUtils.ts +69 -0
- package/tsconfig.json +1 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
2
|
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
|
-
import { AtomicText, AtomicIcon
|
|
3
|
+
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
4
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
4
5
|
import { DEFAULT_FILTERS, type FilterOption } from "../constants";
|
|
5
6
|
|
|
6
7
|
interface FilterPickerProps {
|
|
7
8
|
selectedFilter: string;
|
|
8
|
-
onSelectFilter: (
|
|
9
|
-
filters?:
|
|
9
|
+
onSelectFilter: (option: FilterOption) => void;
|
|
10
|
+
filters?: FilterOption[];
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export const FilterPicker: React.FC<FilterPickerProps> = ({
|
|
@@ -16,7 +17,7 @@ export const FilterPicker: React.FC<FilterPickerProps> = ({
|
|
|
16
17
|
}) => {
|
|
17
18
|
const tokens = useAppDesignTokens();
|
|
18
19
|
|
|
19
|
-
const styles = StyleSheet.create({
|
|
20
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
20
21
|
container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
|
|
21
22
|
grid: { flexDirection: "row", flexWrap: "wrap", gap: tokens.spacing.sm },
|
|
22
23
|
filter: {
|
|
@@ -33,31 +34,37 @@ export const FilterPicker: React.FC<FilterPickerProps> = ({
|
|
|
33
34
|
borderColor: tokens.colors.primary,
|
|
34
35
|
backgroundColor: tokens.colors.primary + "10",
|
|
35
36
|
},
|
|
36
|
-
});
|
|
37
|
+
}), [tokens]);
|
|
37
38
|
|
|
38
39
|
return (
|
|
39
40
|
<View style={styles.container}>
|
|
40
41
|
<AtomicText type="headlineSmall">Filters</AtomicText>
|
|
41
42
|
<View style={styles.grid}>
|
|
42
|
-
{filters.map((f) =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/>
|
|
53
|
-
<AtomicText
|
|
54
|
-
type="labelSmall"
|
|
55
|
-
color={selectedFilter === f.id ? "primary" : "textSecondary"}
|
|
43
|
+
{filters.map((f) => {
|
|
44
|
+
const isActive = selectedFilter === f.id;
|
|
45
|
+
return (
|
|
46
|
+
<TouchableOpacity
|
|
47
|
+
key={f.id}
|
|
48
|
+
style={[styles.filter, isActive && styles.active]}
|
|
49
|
+
onPress={() => onSelectFilter(f)}
|
|
50
|
+
accessibilityLabel={f.name}
|
|
51
|
+
accessibilityRole="button"
|
|
52
|
+
accessibilityState={{ selected: isActive }}
|
|
56
53
|
>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
<AtomicIcon
|
|
55
|
+
name={f.icon as "close"}
|
|
56
|
+
size="lg"
|
|
57
|
+
color={isActive ? "primary" : "textSecondary"}
|
|
58
|
+
/>
|
|
59
|
+
<AtomicText
|
|
60
|
+
type="labelSmall"
|
|
61
|
+
color={isActive ? "primary" : "textSecondary"}
|
|
62
|
+
>
|
|
63
|
+
{f.name}
|
|
64
|
+
</AtomicText>
|
|
65
|
+
</TouchableOpacity>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
61
68
|
</View>
|
|
62
69
|
</View>
|
|
63
70
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
2
|
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
|
-
import { AtomicText, AtomicIcon
|
|
3
|
+
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
4
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
4
5
|
|
|
5
6
|
interface FontControlsProps {
|
|
6
7
|
fontSize: number;
|
|
@@ -8,9 +9,19 @@ interface FontControlsProps {
|
|
|
8
9
|
fonts: readonly string[];
|
|
9
10
|
onFontSizeChange: (size: number) => void;
|
|
10
11
|
onFontSelect: (font: string) => void;
|
|
11
|
-
styles:
|
|
12
|
+
styles: {
|
|
13
|
+
controlsPanel: object;
|
|
14
|
+
fontRow: object;
|
|
15
|
+
fontChip: object;
|
|
16
|
+
fontChipActive: object;
|
|
17
|
+
[key: string]: object;
|
|
18
|
+
};
|
|
12
19
|
}
|
|
13
20
|
|
|
21
|
+
const MIN_FONT_SIZE = 12;
|
|
22
|
+
const MAX_FONT_SIZE = 128;
|
|
23
|
+
const FONT_SIZE_STEP = 4;
|
|
24
|
+
|
|
14
25
|
export const FontControls: React.FC<FontControlsProps> = ({
|
|
15
26
|
fontSize,
|
|
16
27
|
selectedFont,
|
|
@@ -20,9 +31,14 @@ export const FontControls: React.FC<FontControlsProps> = ({
|
|
|
20
31
|
styles: externalStyles,
|
|
21
32
|
}) => {
|
|
22
33
|
const tokens = useAppDesignTokens();
|
|
23
|
-
|
|
24
|
-
const styles = StyleSheet.create({
|
|
25
|
-
|
|
34
|
+
|
|
35
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
36
|
+
stepRow: {
|
|
37
|
+
flexDirection: "row",
|
|
38
|
+
gap: tokens.spacing.sm,
|
|
39
|
+
marginBottom: tokens.spacing.md,
|
|
40
|
+
},
|
|
41
|
+
stepBtn: {
|
|
26
42
|
padding: tokens.spacing.sm,
|
|
27
43
|
backgroundColor: tokens.colors.surface,
|
|
28
44
|
borderRadius: tokens.borders.radius.sm,
|
|
@@ -30,29 +46,58 @@ export const FontControls: React.FC<FontControlsProps> = ({
|
|
|
30
46
|
borderColor: tokens.colors.border,
|
|
31
47
|
minWidth: 44,
|
|
32
48
|
alignItems: "center",
|
|
33
|
-
}
|
|
34
|
-
|
|
49
|
+
},
|
|
50
|
+
sizeRow: {
|
|
51
|
+
flexDirection: "row",
|
|
52
|
+
alignItems: "center",
|
|
53
|
+
justifyContent: "space-between",
|
|
54
|
+
marginBottom: tokens.spacing.sm,
|
|
55
|
+
},
|
|
56
|
+
sizeLabel: {
|
|
57
|
+
flexDirection: "row",
|
|
58
|
+
alignItems: "center",
|
|
59
|
+
gap: tokens.spacing.xs,
|
|
60
|
+
},
|
|
61
|
+
}), [tokens]);
|
|
35
62
|
|
|
36
63
|
return (
|
|
37
64
|
<View style={externalStyles.controlsPanel}>
|
|
38
|
-
<View style={
|
|
39
|
-
<View style={
|
|
40
|
-
<AtomicIcon name="
|
|
41
|
-
<AtomicText type="labelMedium" color="textSecondary">
|
|
65
|
+
<View style={styles.sizeRow}>
|
|
66
|
+
<View style={styles.sizeLabel}>
|
|
67
|
+
<AtomicIcon name="edit" size="sm" color="textSecondary" />
|
|
68
|
+
<AtomicText type="labelMedium" color="textSecondary">
|
|
69
|
+
Text Size
|
|
70
|
+
</AtomicText>
|
|
42
71
|
</View>
|
|
43
|
-
<AtomicText fontWeight="bold" color="primary">
|
|
72
|
+
<AtomicText fontWeight="bold" color="primary">
|
|
73
|
+
{fontSize}px
|
|
74
|
+
</AtomicText>
|
|
44
75
|
</View>
|
|
45
76
|
|
|
46
|
-
<View style={
|
|
47
|
-
<TouchableOpacity
|
|
48
|
-
|
|
77
|
+
<View style={styles.stepRow}>
|
|
78
|
+
<TouchableOpacity
|
|
79
|
+
style={styles.stepBtn}
|
|
80
|
+
onPress={() => onFontSizeChange(Math.max(MIN_FONT_SIZE, fontSize - FONT_SIZE_STEP))}
|
|
81
|
+
accessibilityLabel="Decrease font size"
|
|
82
|
+
accessibilityRole="button"
|
|
83
|
+
>
|
|
84
|
+
<AtomicText fontWeight="bold">−</AtomicText>
|
|
49
85
|
</TouchableOpacity>
|
|
50
|
-
<TouchableOpacity
|
|
86
|
+
<TouchableOpacity
|
|
87
|
+
style={styles.stepBtn}
|
|
88
|
+
onPress={() => onFontSizeChange(Math.min(MAX_FONT_SIZE, fontSize + FONT_SIZE_STEP))}
|
|
89
|
+
accessibilityLabel="Increase font size"
|
|
90
|
+
accessibilityRole="button"
|
|
91
|
+
>
|
|
51
92
|
<AtomicText fontWeight="bold">+</AtomicText>
|
|
52
93
|
</TouchableOpacity>
|
|
53
94
|
</View>
|
|
54
95
|
|
|
55
|
-
<AtomicText
|
|
96
|
+
<AtomicText
|
|
97
|
+
type="labelMedium"
|
|
98
|
+
color="textSecondary"
|
|
99
|
+
style={{ marginBottom: tokens.spacing.xs }}
|
|
100
|
+
>
|
|
56
101
|
Font Style
|
|
57
102
|
</AtomicText>
|
|
58
103
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
@@ -60,13 +105,19 @@ export const FontControls: React.FC<FontControlsProps> = ({
|
|
|
60
105
|
{fonts.map((font) => (
|
|
61
106
|
<TouchableOpacity
|
|
62
107
|
key={font}
|
|
63
|
-
style={[
|
|
108
|
+
style={[
|
|
109
|
+
externalStyles.fontChip,
|
|
110
|
+
selectedFont === font && externalStyles.fontChipActive,
|
|
111
|
+
]}
|
|
64
112
|
onPress={() => onFontSelect(font)}
|
|
113
|
+
accessibilityLabel={`Font: ${font}`}
|
|
114
|
+
accessibilityRole="button"
|
|
115
|
+
accessibilityState={{ selected: selectedFont === font }}
|
|
65
116
|
>
|
|
66
117
|
<AtomicText
|
|
67
118
|
fontWeight="bold"
|
|
68
119
|
color={selectedFont === font ? "onPrimary" : "textSecondary"}
|
|
69
|
-
style={{ fontFamily: font }}
|
|
120
|
+
style={{ fontFamily: font === "System" ? undefined : font }}
|
|
70
121
|
>
|
|
71
122
|
{font}
|
|
72
123
|
</AtomicText>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
2
|
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
|
-
import { AtomicText, AtomicIcon
|
|
3
|
+
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
4
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
4
5
|
import { Layer, TextLayer } from "../types";
|
|
5
6
|
|
|
6
7
|
interface LayerManagerProps {
|
|
@@ -8,6 +9,9 @@ interface LayerManagerProps {
|
|
|
8
9
|
activeLayerId: string | null;
|
|
9
10
|
onSelectLayer: (id: string) => void;
|
|
10
11
|
onDeleteLayer: (id: string) => void;
|
|
12
|
+
onDuplicateLayer?: (id: string) => void;
|
|
13
|
+
onMoveLayerUp?: (id: string) => void;
|
|
14
|
+
onMoveLayerDown?: (id: string) => void;
|
|
11
15
|
t: (key: string) => string;
|
|
12
16
|
}
|
|
13
17
|
|
|
@@ -16,16 +20,19 @@ export const LayerManager: React.FC<LayerManagerProps> = ({
|
|
|
16
20
|
activeLayerId,
|
|
17
21
|
onSelectLayer,
|
|
18
22
|
onDeleteLayer,
|
|
23
|
+
onDuplicateLayer,
|
|
24
|
+
onMoveLayerUp,
|
|
25
|
+
onMoveLayerDown,
|
|
19
26
|
t,
|
|
20
27
|
}) => {
|
|
21
28
|
const tokens = useAppDesignTokens();
|
|
22
29
|
|
|
23
|
-
const styles = StyleSheet.create({
|
|
30
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
24
31
|
container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
|
|
25
32
|
item: {
|
|
26
33
|
flexDirection: "row",
|
|
27
34
|
alignItems: "center",
|
|
28
|
-
padding: tokens.spacing.
|
|
35
|
+
padding: tokens.spacing.sm,
|
|
29
36
|
backgroundColor: tokens.colors.surfaceVariant,
|
|
30
37
|
borderRadius: tokens.borders.radius.md,
|
|
31
38
|
marginBottom: tokens.spacing.xs,
|
|
@@ -37,41 +44,119 @@ export const LayerManager: React.FC<LayerManagerProps> = ({
|
|
|
37
44
|
backgroundColor: tokens.colors.primary + "10",
|
|
38
45
|
},
|
|
39
46
|
info: { flex: 1, marginLeft: tokens.spacing.sm },
|
|
40
|
-
|
|
47
|
+
actions: {
|
|
48
|
+
flexDirection: "row",
|
|
49
|
+
alignItems: "center",
|
|
50
|
+
gap: tokens.spacing.xs,
|
|
51
|
+
},
|
|
52
|
+
actionBtn: {
|
|
53
|
+
padding: tokens.spacing.xs,
|
|
54
|
+
borderRadius: tokens.borders.radius.sm,
|
|
55
|
+
},
|
|
56
|
+
}), [tokens]);
|
|
57
|
+
|
|
58
|
+
const sortedLayers = [...layers].reverse(); // top layer first in list
|
|
41
59
|
|
|
42
60
|
return (
|
|
43
61
|
<View style={styles.container}>
|
|
44
62
|
<AtomicText type="headlineSmall">Layers</AtomicText>
|
|
45
63
|
<ScrollView showsVerticalScrollIndicator={false}>
|
|
46
|
-
{
|
|
47
|
-
<AtomicText
|
|
64
|
+
{sortedLayers.length === 0 ? (
|
|
65
|
+
<AtomicText
|
|
66
|
+
color="textSecondary"
|
|
67
|
+
style={{ textAlign: "center", padding: tokens.spacing.xl }}
|
|
68
|
+
>
|
|
48
69
|
No layers yet
|
|
49
70
|
</AtomicText>
|
|
50
71
|
) : (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
sortedLayers.map((layer, idx) => {
|
|
73
|
+
const isActive = activeLayerId === layer.id;
|
|
74
|
+
const label =
|
|
75
|
+
layer.type === "text"
|
|
76
|
+
? (layer as TextLayer).text || t("editor.untitled") || "Untitled"
|
|
77
|
+
: "Sticker";
|
|
78
|
+
const isTop = idx === 0;
|
|
79
|
+
const isBottom = idx === sortedLayers.length - 1;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<TouchableOpacity
|
|
83
|
+
key={layer.id}
|
|
84
|
+
style={[styles.item, isActive && styles.active]}
|
|
85
|
+
onPress={() => onSelectLayer(layer.id)}
|
|
86
|
+
accessibilityLabel={`${layer.type} layer: ${label}`}
|
|
87
|
+
accessibilityRole="button"
|
|
88
|
+
accessibilityState={{ selected: isActive }}
|
|
89
|
+
>
|
|
90
|
+
<AtomicIcon
|
|
91
|
+
name={layer.type === "text" ? "edit" : "image"}
|
|
92
|
+
size="sm"
|
|
93
|
+
color={isActive ? "primary" : "textSecondary"}
|
|
94
|
+
/>
|
|
95
|
+
<View style={styles.info}>
|
|
96
|
+
<AtomicText type="labelSmall" color="textSecondary">
|
|
97
|
+
{layer.type.toUpperCase()}
|
|
98
|
+
</AtomicText>
|
|
99
|
+
<AtomicText fontWeight="bold" numberOfLines={1}>
|
|
100
|
+
{label}
|
|
101
|
+
</AtomicText>
|
|
102
|
+
</View>
|
|
103
|
+
|
|
104
|
+
<View style={styles.actions}>
|
|
105
|
+
{onMoveLayerUp && (
|
|
106
|
+
<TouchableOpacity
|
|
107
|
+
style={styles.actionBtn}
|
|
108
|
+
onPress={() => onMoveLayerUp(layer.id)}
|
|
109
|
+
disabled={isTop}
|
|
110
|
+
accessibilityLabel="Move layer up"
|
|
111
|
+
accessibilityRole="button"
|
|
112
|
+
>
|
|
113
|
+
<AtomicIcon
|
|
114
|
+
name="chevron-forward"
|
|
115
|
+
size="sm"
|
|
116
|
+
color={isTop ? "textSecondary" : "textPrimary"}
|
|
117
|
+
/>
|
|
118
|
+
</TouchableOpacity>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{onMoveLayerDown && (
|
|
122
|
+
<TouchableOpacity
|
|
123
|
+
style={styles.actionBtn}
|
|
124
|
+
onPress={() => onMoveLayerDown(layer.id)}
|
|
125
|
+
disabled={isBottom}
|
|
126
|
+
accessibilityLabel="Move layer down"
|
|
127
|
+
accessibilityRole="button"
|
|
128
|
+
>
|
|
129
|
+
<AtomicIcon
|
|
130
|
+
name="chevron-back"
|
|
131
|
+
size="sm"
|
|
132
|
+
color={isBottom ? "textSecondary" : "textPrimary"}
|
|
133
|
+
/>
|
|
134
|
+
</TouchableOpacity>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{onDuplicateLayer && (
|
|
138
|
+
<TouchableOpacity
|
|
139
|
+
style={styles.actionBtn}
|
|
140
|
+
onPress={() => onDuplicateLayer(layer.id)}
|
|
141
|
+
accessibilityLabel={`Duplicate ${label}`}
|
|
142
|
+
accessibilityRole="button"
|
|
143
|
+
>
|
|
144
|
+
<AtomicIcon name="copy" size="sm" color="textSecondary" />
|
|
145
|
+
</TouchableOpacity>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
<TouchableOpacity
|
|
149
|
+
style={styles.actionBtn}
|
|
150
|
+
onPress={() => onDeleteLayer(layer.id)}
|
|
151
|
+
accessibilityLabel={`Delete ${label}`}
|
|
152
|
+
accessibilityRole="button"
|
|
153
|
+
>
|
|
154
|
+
<AtomicIcon name="trash-outline" size="sm" color="error" />
|
|
155
|
+
</TouchableOpacity>
|
|
156
|
+
</View>
|
|
72
157
|
</TouchableOpacity>
|
|
73
|
-
|
|
74
|
-
)
|
|
158
|
+
);
|
|
159
|
+
})
|
|
75
160
|
)}
|
|
76
161
|
</ScrollView>
|
|
77
162
|
</View>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { useRef, useState } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
4
|
+
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
5
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
6
|
+
|
|
7
|
+
interface SliderProps {
|
|
8
|
+
label: string;
|
|
9
|
+
value: number;
|
|
10
|
+
min: number;
|
|
11
|
+
max: number;
|
|
12
|
+
step?: number;
|
|
13
|
+
onValueChange: (val: number) => void;
|
|
14
|
+
formatValue?: (val: number) => string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Slider: React.FC<SliderProps> = ({
|
|
18
|
+
label,
|
|
19
|
+
value,
|
|
20
|
+
min,
|
|
21
|
+
max,
|
|
22
|
+
step = 0.05,
|
|
23
|
+
onValueChange,
|
|
24
|
+
formatValue,
|
|
25
|
+
}) => {
|
|
26
|
+
const tokens = useAppDesignTokens();
|
|
27
|
+
const [trackWidth, setTrackWidth] = useState(200);
|
|
28
|
+
const trackWidthRef = useRef(200);
|
|
29
|
+
const startValueRef = useRef(value);
|
|
30
|
+
const valueRef = useRef(value);
|
|
31
|
+
valueRef.current = value;
|
|
32
|
+
|
|
33
|
+
const clamp = (v: number) => Math.max(min, Math.min(max, v));
|
|
34
|
+
const snap = (v: number) => parseFloat((Math.round(v / step) * step).toFixed(4));
|
|
35
|
+
|
|
36
|
+
const panGesture = Gesture.Pan()
|
|
37
|
+
.runOnJS(true)
|
|
38
|
+
.onStart(() => {
|
|
39
|
+
startValueRef.current = valueRef.current;
|
|
40
|
+
})
|
|
41
|
+
.onUpdate((e) => {
|
|
42
|
+
const ratio = e.translationX / trackWidthRef.current;
|
|
43
|
+
const delta = ratio * (max - min);
|
|
44
|
+
onValueChange(snap(clamp(startValueRef.current + delta)));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const percent = Math.max(0, Math.min(1, (value - min) / (max - min)));
|
|
48
|
+
const thumbOffset = percent * trackWidth - 12;
|
|
49
|
+
const displayValue = formatValue ? formatValue(value) : value.toFixed(1);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<View style={{ gap: tokens.spacing.xs }}>
|
|
53
|
+
<View
|
|
54
|
+
style={{
|
|
55
|
+
flexDirection: "row",
|
|
56
|
+
justifyContent: "space-between",
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<AtomicText type="labelMedium" color="textSecondary">
|
|
61
|
+
{label}
|
|
62
|
+
</AtomicText>
|
|
63
|
+
<AtomicText type="labelMedium" color="primary" fontWeight="bold">
|
|
64
|
+
{displayValue}
|
|
65
|
+
</AtomicText>
|
|
66
|
+
</View>
|
|
67
|
+
<GestureDetector gesture={panGesture}>
|
|
68
|
+
<View
|
|
69
|
+
style={{ height: 44, justifyContent: "center", paddingHorizontal: 12 }}
|
|
70
|
+
onLayout={(e) => {
|
|
71
|
+
const w = Math.max(1, e.nativeEvent.layout.width - 24);
|
|
72
|
+
setTrackWidth(w);
|
|
73
|
+
trackWidthRef.current = w;
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
{/* Track background */}
|
|
77
|
+
<View
|
|
78
|
+
style={{
|
|
79
|
+
height: 4,
|
|
80
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
81
|
+
borderRadius: 2,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{/* Filled portion */}
|
|
85
|
+
<View
|
|
86
|
+
style={{
|
|
87
|
+
width: `${percent * 100}%`,
|
|
88
|
+
height: "100%",
|
|
89
|
+
backgroundColor: tokens.colors.primary,
|
|
90
|
+
borderRadius: 2,
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
</View>
|
|
94
|
+
{/* Thumb */}
|
|
95
|
+
<View
|
|
96
|
+
style={{
|
|
97
|
+
position: "absolute",
|
|
98
|
+
left: thumbOffset + 12,
|
|
99
|
+
width: 24,
|
|
100
|
+
height: 24,
|
|
101
|
+
borderRadius: 12,
|
|
102
|
+
backgroundColor: tokens.colors.primary,
|
|
103
|
+
borderWidth: 2.5,
|
|
104
|
+
borderColor: tokens.colors.surface,
|
|
105
|
+
top: 10,
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
</View>
|
|
109
|
+
</GestureDetector>
|
|
110
|
+
</View>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
2
|
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
|
-
import { AtomicText
|
|
3
|
+
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
4
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
4
5
|
import { DEFAULT_STICKERS } from "../constants";
|
|
5
6
|
|
|
6
7
|
interface StickerPickerProps {
|
|
@@ -14,7 +15,7 @@ export const StickerPicker: React.FC<StickerPickerProps> = ({
|
|
|
14
15
|
}) => {
|
|
15
16
|
const tokens = useAppDesignTokens();
|
|
16
17
|
|
|
17
|
-
const styles = StyleSheet.create({
|
|
18
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
18
19
|
container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
|
|
19
20
|
grid: { flexDirection: "row", flexWrap: "wrap", gap: tokens.spacing.sm },
|
|
20
21
|
sticker: {
|
|
@@ -25,7 +26,7 @@ export const StickerPicker: React.FC<StickerPickerProps> = ({
|
|
|
25
26
|
alignItems: "center",
|
|
26
27
|
justifyContent: "center",
|
|
27
28
|
},
|
|
28
|
-
});
|
|
29
|
+
}), [tokens]);
|
|
29
30
|
|
|
30
31
|
return (
|
|
31
32
|
<View style={styles.container}>
|