@umituz/react-native-photo-editor 2.0.23 → 2.0.25

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.
Files changed (57) hide show
  1. package/package.json +1 -1
  2. package/src/PhotoEditor.tsx +43 -137
  3. package/src/application/hooks/useEditor.ts +4 -6
  4. package/src/application/hooks/useEditorUI.ts +8 -5
  5. package/src/application/stores/EditorStore.ts +17 -6
  6. package/src/domain/entities/Layer.entity.ts +86 -0
  7. package/src/domain/entities/{Layer.ts → Layer.legacy.ts} +3 -3
  8. package/src/domain/entities/StickerLayer.entity.ts +37 -0
  9. package/src/domain/entities/TextLayer.entity.ts +58 -0
  10. package/src/domain/entities/index.ts +9 -0
  11. package/src/domain/services/History.service.ts +69 -0
  12. package/src/domain/services/LayerFactory.service.ts +81 -0
  13. package/src/domain/services/LayerRepository.service.ts +85 -0
  14. package/src/domain/services/LayerService.ts +1 -1
  15. package/src/domain/types.ts +39 -0
  16. package/src/domain/value-objects/FilterSettings.vo.ts +89 -0
  17. package/src/domain/value-objects/LayerDefaults.vo.ts +56 -0
  18. package/src/domain/value-objects/Transform.vo.ts +61 -0
  19. package/src/domain/value-objects/index.ts +13 -0
  20. package/src/index.ts +4 -4
  21. package/src/infrastructure/gesture/createTransformGesture.ts +127 -0
  22. package/src/infrastructure/gesture/useTransformGesture.ts +7 -13
  23. package/src/presentation/components/DraggableLayer.tsx +13 -13
  24. package/src/presentation/components/EditorCanvas.tsx +5 -5
  25. package/src/presentation/components/EditorContent.tsx +72 -0
  26. package/src/presentation/components/EditorHeader.tsx +48 -0
  27. package/src/presentation/components/EditorSheets.tsx +85 -0
  28. package/src/presentation/components/FontControls.tsx +2 -2
  29. package/src/presentation/components/sheets/AdjustmentsSheet.tsx +4 -4
  30. package/src/presentation/components/sheets/FilterSheet.tsx +1 -1
  31. package/src/presentation/components/sheets/LayerManager.tsx +3 -4
  32. package/src/presentation/components/sheets/TextEditorSheet.tsx +1 -1
  33. package/src/types.ts +8 -18
  34. package/src/utils/constants.ts +84 -0
  35. package/src/utils/formatters.ts +29 -0
  36. package/src/utils/helpers.ts +51 -0
  37. package/src/utils/index.ts +9 -0
  38. package/src/utils/validators.ts +38 -0
  39. package/ARCHITECTURE.md +0 -104
  40. package/MIGRATION.md +0 -100
  41. package/src/components/AIMagicSheet.tsx +0 -107
  42. package/src/components/AdjustmentsSheet.tsx +0 -108
  43. package/src/components/ColorPicker.tsx +0 -77
  44. package/src/components/DraggableSticker.tsx +0 -161
  45. package/src/components/DraggableText.tsx +0 -181
  46. package/src/components/EditorCanvas.tsx +0 -106
  47. package/src/components/EditorToolbar.tsx +0 -155
  48. package/src/components/FilterPicker.tsx +0 -73
  49. package/src/components/FontControls.tsx +0 -132
  50. package/src/components/LayerManager.tsx +0 -164
  51. package/src/components/Slider.tsx +0 -112
  52. package/src/components/StickerPicker.tsx +0 -47
  53. package/src/components/TextEditorSheet.tsx +0 -160
  54. package/src/core/HistoryManager.ts +0 -53
  55. package/src/hooks/usePhotoEditor.ts +0 -172
  56. package/src/hooks/usePhotoEditorUI.ts +0 -162
  57. package/src/infrastructure/history/HistoryManager.ts +0 -38
@@ -1,164 +0,0 @@
1
- import React, { useMemo } from "react";
2
- import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
3
- import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
4
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
5
- import { Layer, TextLayer } from "../types";
6
-
7
- interface LayerManagerProps {
8
- layers: Layer[];
9
- activeLayerId: string | null;
10
- onSelectLayer: (id: string) => void;
11
- onDeleteLayer: (id: string) => void;
12
- onDuplicateLayer?: (id: string) => void;
13
- onMoveLayerUp?: (id: string) => void;
14
- onMoveLayerDown?: (id: string) => void;
15
- t: (key: string) => string;
16
- }
17
-
18
- export const LayerManager: React.FC<LayerManagerProps> = ({
19
- layers,
20
- activeLayerId,
21
- onSelectLayer,
22
- onDeleteLayer,
23
- onDuplicateLayer,
24
- onMoveLayerUp,
25
- onMoveLayerDown,
26
- t,
27
- }) => {
28
- const tokens = useAppDesignTokens();
29
-
30
- const styles = useMemo(() => StyleSheet.create({
31
- container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
32
- item: {
33
- flexDirection: "row",
34
- alignItems: "center",
35
- padding: tokens.spacing.sm,
36
- backgroundColor: tokens.colors.surfaceVariant,
37
- borderRadius: tokens.borders.radius.md,
38
- marginBottom: tokens.spacing.xs,
39
- borderWidth: 2,
40
- borderColor: "transparent",
41
- },
42
- active: {
43
- borderColor: tokens.colors.primary,
44
- backgroundColor: tokens.colors.primary + "10",
45
- },
46
- info: { flex: 1, marginLeft: tokens.spacing.sm },
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
59
-
60
- return (
61
- <View style={styles.container}>
62
- <AtomicText type="headlineSmall">Layers</AtomicText>
63
- <ScrollView showsVerticalScrollIndicator={false}>
64
- {sortedLayers.length === 0 ? (
65
- <AtomicText
66
- color="textSecondary"
67
- style={{ textAlign: "center", padding: tokens.spacing.xl }}
68
- >
69
- No layers yet
70
- </AtomicText>
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("photo_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>
157
- </TouchableOpacity>
158
- );
159
- })
160
- )}
161
- </ScrollView>
162
- </View>
163
- );
164
- };
@@ -1,112 +0,0 @@
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,47 +0,0 @@
1
- import React, { useMemo } from "react";
2
- import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
3
- import { AtomicText } from "@umituz/react-native-design-system/atoms";
4
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
5
- import { DEFAULT_STICKERS } from "../constants";
6
-
7
- interface StickerPickerProps {
8
- onSelectSticker: (sticker: string) => void;
9
- stickers?: readonly string[];
10
- }
11
-
12
- export const StickerPicker: React.FC<StickerPickerProps> = ({
13
- onSelectSticker,
14
- stickers = DEFAULT_STICKERS,
15
- }) => {
16
- const tokens = useAppDesignTokens();
17
-
18
- const styles = useMemo(() => StyleSheet.create({
19
- container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
20
- grid: { flexDirection: "row", flexWrap: "wrap", gap: tokens.spacing.sm },
21
- sticker: {
22
- width: 50,
23
- height: 50,
24
- borderRadius: tokens.borders.radius.sm,
25
- backgroundColor: tokens.colors.surfaceVariant,
26
- alignItems: "center",
27
- justifyContent: "center",
28
- },
29
- }), [tokens]);
30
-
31
- return (
32
- <View style={styles.container}>
33
- <AtomicText type="headlineSmall">Stickers</AtomicText>
34
- <ScrollView showsVerticalScrollIndicator={false}>
35
- <View style={styles.grid}>
36
- {stickers.map((s, i) => (
37
- <TouchableOpacity key={`${s}-${i}`} style={styles.sticker} onPress={() => onSelectSticker(s)}>
38
- <AtomicText style={{ fontSize: 32 }}>{s}</AtomicText>
39
- </TouchableOpacity>
40
- ))}
41
- </View>
42
- </ScrollView>
43
- </View>
44
- );
45
- };
46
-
47
- export default React.memo(StickerPicker);
@@ -1,160 +0,0 @@
1
- import React, { useMemo } from "react";
2
- import { View, TextInput, TouchableOpacity, StyleSheet } from "react-native";
3
- import { AtomicText, AtomicButton } from "@umituz/react-native-design-system/atoms";
4
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
5
- import { ColorPicker } from "./ColorPicker";
6
- import type { TextAlign } from "../types";
7
-
8
- interface TextEditorSheetProps {
9
- value: string;
10
- onChange: (text: string) => void;
11
- onSave: () => void;
12
- t: (key: string) => string;
13
- color?: string;
14
- onColorChange?: (color: string) => void;
15
- textAlign?: TextAlign;
16
- onTextAlignChange?: (align: TextAlign) => void;
17
- isBold?: boolean;
18
- onBoldChange?: (bold: boolean) => void;
19
- isItalic?: boolean;
20
- onItalicChange?: (italic: boolean) => void;
21
- }
22
-
23
- const ALIGN_OPTIONS: { value: TextAlign; icon: string }[] = [
24
- { value: "left", icon: "«" },
25
- { value: "center", icon: "≡" },
26
- { value: "right", icon: "»" },
27
- ];
28
-
29
- export const TextEditorSheet: React.FC<TextEditorSheetProps> = ({
30
- value,
31
- onChange,
32
- onSave,
33
- t,
34
- color = "#FFFFFF",
35
- onColorChange,
36
- textAlign = "center",
37
- onTextAlignChange,
38
- isBold = false,
39
- onBoldChange,
40
- isItalic = false,
41
- onItalicChange,
42
- }) => {
43
- const tokens = useAppDesignTokens();
44
-
45
- const styles = useMemo(() => StyleSheet.create({
46
- container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
47
- input: {
48
- backgroundColor: tokens.colors.surfaceVariant,
49
- borderRadius: tokens.borders.radius.md,
50
- padding: tokens.spacing.md,
51
- fontSize: 18,
52
- color: tokens.colors.textPrimary,
53
- textAlign: "center",
54
- minHeight: 90,
55
- },
56
- row: {
57
- flexDirection: "row",
58
- alignItems: "center",
59
- gap: tokens.spacing.sm,
60
- },
61
- styleBtn: {
62
- width: 44,
63
- height: 44,
64
- borderRadius: tokens.borders.radius.sm,
65
- borderWidth: 1.5,
66
- borderColor: tokens.colors.border,
67
- alignItems: "center",
68
- justifyContent: "center",
69
- backgroundColor: tokens.colors.surfaceVariant,
70
- },
71
- styleBtnActive: {
72
- borderColor: tokens.colors.primary,
73
- backgroundColor: tokens.colors.primary + "20",
74
- },
75
- }), [tokens]);
76
-
77
- return (
78
- <View style={styles.container}>
79
- <AtomicText type="headlineSmall">{t("photo_editor.add_text") || "Edit Text"}</AtomicText>
80
-
81
- <TextInput
82
- value={value}
83
- onChangeText={onChange}
84
- placeholder={t("photo_editor.tap_to_edit") || "Enter text…"}
85
- placeholderTextColor={tokens.colors.textSecondary}
86
- style={styles.input}
87
- multiline
88
- autoFocus
89
- />
90
-
91
- {/* Style row: Bold, Italic, Alignment */}
92
- <View style={styles.row}>
93
- {onBoldChange && (
94
- <TouchableOpacity
95
- style={[styles.styleBtn, isBold && styles.styleBtnActive]}
96
- onPress={() => onBoldChange(!isBold)}
97
- accessibilityLabel="Bold"
98
- accessibilityRole="button"
99
- accessibilityState={{ selected: isBold }}
100
- >
101
- <AtomicText fontWeight="bold" color={isBold ? "primary" : "textSecondary"}>
102
- B
103
- </AtomicText>
104
- </TouchableOpacity>
105
- )}
106
-
107
- {onItalicChange && (
108
- <TouchableOpacity
109
- style={[styles.styleBtn, isItalic && styles.styleBtnActive]}
110
- onPress={() => onItalicChange(!isItalic)}
111
- accessibilityLabel="Italic"
112
- accessibilityRole="button"
113
- accessibilityState={{ selected: isItalic }}
114
- >
115
- <AtomicText
116
- color={isItalic ? "primary" : "textSecondary"}
117
- style={{ fontStyle: "italic" }}
118
- >
119
- I
120
- </AtomicText>
121
- </TouchableOpacity>
122
- )}
123
-
124
- {onTextAlignChange && (
125
- <View style={[styles.row, { marginLeft: tokens.spacing.sm }]}>
126
- {ALIGN_OPTIONS.map(({ value: align, icon }) => (
127
- <TouchableOpacity
128
- key={align}
129
- style={[styles.styleBtn, textAlign === align && styles.styleBtnActive]}
130
- onPress={() => onTextAlignChange(align)}
131
- accessibilityLabel={`Align ${align}`}
132
- accessibilityRole="button"
133
- accessibilityState={{ selected: textAlign === align }}
134
- >
135
- <AtomicText color={textAlign === align ? "primary" : "textSecondary"}>
136
- {icon}
137
- </AtomicText>
138
- </TouchableOpacity>
139
- ))}
140
- </View>
141
- )}
142
- </View>
143
-
144
- {/* Color picker */}
145
- {onColorChange && (
146
- <ColorPicker
147
- label="Text Color"
148
- selectedColor={color}
149
- onSelectColor={onColorChange}
150
- />
151
- )}
152
-
153
- <AtomicButton variant="primary" onPress={onSave}>
154
- {t("common.save") || "Save"}
155
- </AtomicButton>
156
- </View>
157
- );
158
- };
159
-
160
- export default React.memo(TextEditorSheet);
@@ -1,53 +0,0 @@
1
- /**
2
- * History Manager for Undo/Redo functionality
3
- */
4
-
5
- export interface HistoryState<T> {
6
- past: T[];
7
- present: T;
8
- future: T[];
9
- }
10
-
11
- export class HistoryManager<T> {
12
- private readonly maxHistory = 20;
13
-
14
- createInitialState(initialValue: T): HistoryState<T> {
15
- return { past: [], present: initialValue, future: [] };
16
- }
17
-
18
- push(history: HistoryState<T>, newValue: T): HistoryState<T> {
19
- return {
20
- past: [...history.past.slice(-this.maxHistory + 1), history.present],
21
- present: newValue,
22
- future: [],
23
- };
24
- }
25
-
26
- undo(history: HistoryState<T>): HistoryState<T> {
27
- if (history.past.length === 0) return history;
28
- const previous = history.past[history.past.length - 1];
29
- return {
30
- past: history.past.slice(0, -1),
31
- present: previous,
32
- future: [history.present, ...history.future],
33
- };
34
- }
35
-
36
- redo(history: HistoryState<T>): HistoryState<T> {
37
- if (history.future.length === 0) return history;
38
- const next = history.future[0];
39
- return {
40
- past: [...history.past, history.present],
41
- present: next,
42
- future: history.future.slice(1),
43
- };
44
- }
45
-
46
- canUndo(history: HistoryState<T>): boolean {
47
- return history.past.length > 0;
48
- }
49
-
50
- canRedo(history: HistoryState<T>): boolean {
51
- return history.future.length > 0;
52
- }
53
- }