@umituz/react-native-photo-editor 1.1.2 → 2.0.1

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
1
  import React 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> = ({
@@ -39,25 +40,31 @@ export const FilterPicker: React.FC<FilterPickerProps> = ({
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
1
  import React 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
-
34
+
24
35
  const styles = StyleSheet.create({
25
- btn: {
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
- }
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
+ },
34
61
  });
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
1
  import React 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,6 +20,9 @@ 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();
@@ -25,7 +32,7 @@ export const LayerManager: React.FC<LayerManagerProps> = ({
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 },
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
+ },
40
56
  });
41
57
 
58
+ const sortedLayers = [...layers].reverse(); // top layer first in list
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
1
  import React 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 {
@@ -1,19 +1,44 @@
1
1
  import React from "react";
2
- import { View, TextInput, StyleSheet } from "react-native";
3
- import { AtomicText, useAppDesignTokens, AtomicButton } from "@umituz/react-native-design-system";
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";
4
7
 
5
8
  interface TextEditorSheetProps {
6
9
  value: string;
7
10
  onChange: (text: string) => void;
8
11
  onSave: () => void;
9
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;
10
21
  }
11
22
 
23
+ const ALIGN_OPTIONS: { value: TextAlign; icon: string }[] = [
24
+ { value: "left", icon: "«" },
25
+ { value: "center", icon: "≡" },
26
+ { value: "right", icon: "»" },
27
+ ];
28
+
12
29
  export const TextEditorSheet: React.FC<TextEditorSheetProps> = ({
13
30
  value,
14
31
  onChange,
15
32
  onSave,
16
33
  t,
34
+ color = "#FFFFFF",
35
+ onColorChange,
36
+ textAlign = "center",
37
+ onTextAlignChange,
38
+ isBold = false,
39
+ onBoldChange,
40
+ isItalic = false,
41
+ onItalicChange,
17
42
  }) => {
18
43
  const tokens = useAppDesignTokens();
19
44
 
@@ -26,26 +51,107 @@ export const TextEditorSheet: React.FC<TextEditorSheetProps> = ({
26
51
  fontSize: 18,
27
52
  color: tokens.colors.textPrimary,
28
53
  textAlign: "center",
29
- minHeight: 120,
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",
30
74
  },
31
75
  });
32
76
 
33
77
  return (
34
78
  <View style={styles.container}>
35
- <AtomicText type="headlineSmall">{t("editor.add_text")}</AtomicText>
36
-
79
+ <AtomicText type="headlineSmall">{t("editor.add_text") || "Edit Text"}</AtomicText>
80
+
37
81
  <TextInput
38
82
  value={value}
39
83
  onChangeText={onChange}
40
- placeholder={t("editor.tap_to_edit")}
84
+ placeholder={t("editor.tap_to_edit") || "Enter text…"}
41
85
  placeholderTextColor={tokens.colors.textSecondary}
42
86
  style={styles.input}
43
87
  multiline
44
88
  autoFocus
45
89
  />
46
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
+
47
153
  <AtomicButton variant="primary" onPress={onSave}>
48
- {t("common.save")}
154
+ {t("common.save") || "Save"}
49
155
  </AtomicButton>
50
156
  </View>
51
157
  );