@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.
@@ -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, useAppDesignTokens } from "@umituz/react-native-design-system";
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: (filterId: string, value: number) => void;
9
- filters?: readonly FilterOption[];
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
- <TouchableOpacity
44
- key={f.id}
45
- style={[styles.filter, selectedFilter === f.id && styles.active]}
46
- onPress={() => onSelectFilter(f.id, f.value)}
47
- >
48
- <AtomicIcon
49
- name={f.icon as "close-circle" | "color-palette" | "contrast" | "time" | "sunny" | "snow"}
50
- size="lg"
51
- color={selectedFilter === f.id ? "primary" : "textSecondary"}
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
- {f.name}
58
- </AtomicText>
59
- </TouchableOpacity>
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, useAppDesignTokens } from "@umituz/react-native-design-system";
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: Record<string, object>;
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
- btn: {
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={externalStyles.sliderRow}>
39
- <View style={externalStyles.sliderLabel}>
40
- <AtomicIcon name="text" size="sm" color="textSecondary" />
41
- <AtomicText type="labelMedium" color="textSecondary">Text Size</AtomicText>
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">{fontSize}px</AtomicText>
72
+ <AtomicText fontWeight="bold" color="primary">
73
+ {fontSize}px
74
+ </AtomicText>
44
75
  </View>
45
76
 
46
- <View style={{ flexDirection: "row", gap: tokens.spacing.sm, marginBottom: tokens.spacing.md }}>
47
- <TouchableOpacity onPress={() => onFontSizeChange(fontSize - 4)} style={styles.btn}>
48
- <AtomicText fontWeight="bold">-</AtomicText>
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 onPress={() => onFontSizeChange(fontSize + 4)} style={styles.btn}>
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 type="labelMedium" color="textSecondary" style={{ marginBottom: tokens.spacing.xs }}>
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={[externalStyles.fontChip, selectedFont === font && externalStyles.fontChipActive]}
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, useAppDesignTokens } from "@umituz/react-native-design-system";
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.md,
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
- {layers.length === 0 ? (
47
- <AtomicText color="textSecondary" style={{ textAlign: "center", padding: tokens.spacing.xl }}>
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
- layers.map((layer) => (
52
- <TouchableOpacity
53
- key={layer.id}
54
- style={[styles.item, activeLayerId === layer.id && styles.active]}
55
- onPress={() => onSelectLayer(layer.id)}
56
- >
57
- <AtomicIcon
58
- name={layer.type === "text" ? "text" : "happy"}
59
- size="md"
60
- color={activeLayerId === layer.id ? "primary" : "textSecondary"}
61
- />
62
- <View style={styles.info}>
63
- <AtomicText type="labelSmall" color="textSecondary">
64
- {layer.type.toUpperCase()}
65
- </AtomicText>
66
- <AtomicText fontWeight="bold" numberOfLines={1}>
67
- {layer.type === "text" ? (layer as TextLayer).text || t("editor.untitled") : "Sticker"}
68
- </AtomicText>
69
- </View>
70
- <TouchableOpacity onPress={() => onDeleteLayer(layer.id)} style={{ padding: tokens.spacing.xs }}>
71
- <AtomicIcon name="trash" size="sm" color="error" />
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
- </TouchableOpacity>
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, useAppDesignTokens } from "@umituz/react-native-design-system";
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}>