@umituz/react-native-video-editor 1.1.64 → 1.1.65

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 (29) hide show
  1. package/package.json +1 -1
  2. package/src/application/services/EditorService.ts +151 -0
  3. package/src/application/usecases/LayerUseCases.ts +192 -0
  4. package/src/infrastructure/repositories/LayerRepositoryImpl.ts +158 -0
  5. package/src/infrastructure/utils/debounce.utils.ts +69 -0
  6. package/src/infrastructure/utils/image-processing.utils.ts +73 -0
  7. package/src/infrastructure/utils/position-calculations.utils.ts +45 -161
  8. package/src/presentation/components/EditorTimeline.tsx +29 -11
  9. package/src/presentation/components/EditorToolPanel.tsx +71 -172
  10. package/src/presentation/components/LayerActionsMenu.tsx +97 -159
  11. package/src/presentation/components/SceneActionsMenu.tsx +34 -44
  12. package/src/presentation/components/SubtitleListPanel.tsx +54 -27
  13. package/src/presentation/components/SubtitleStylePicker.tsx +36 -156
  14. package/src/presentation/components/draggable-layer/LayerContent.tsx +7 -2
  15. package/src/presentation/components/generic/ActionMenu.tsx +110 -0
  16. package/src/presentation/components/generic/Editor.tsx +65 -0
  17. package/src/presentation/components/generic/Selector.tsx +96 -0
  18. package/src/presentation/components/generic/Toolbar.tsx +77 -0
  19. package/src/presentation/components/image-layer/ImagePreview.tsx +7 -1
  20. package/src/presentation/components/shape-layer/ColorPickerHorizontal.tsx +20 -55
  21. package/src/presentation/components/text-layer/ColorPicker.tsx +21 -55
  22. package/src/presentation/components/text-layer/FontSizeSelector.tsx +19 -50
  23. package/src/presentation/components/text-layer/OptionSelector.tsx +18 -55
  24. package/src/presentation/components/text-layer/TextAlignSelector.tsx +24 -51
  25. package/src/presentation/hooks/generic/useForm.ts +99 -0
  26. package/src/presentation/hooks/generic/useList.ts +117 -0
  27. package/src/presentation/hooks/useDraggableLayerGestures.ts +28 -25
  28. package/src/presentation/hooks/useEditorPlayback.ts +19 -2
  29. package/src/presentation/hooks/useMenuActions.tsx +19 -4
@@ -1,17 +1,14 @@
1
1
  /**
2
- * SubtitleStylePicker Component
3
- * Font size, color, background, position pickers + live preview
2
+ * Subtitle Style Picker Component
3
+ * REFACTORED: Uses generic Selector component (110 lines)
4
4
  */
5
5
 
6
6
  import React, { useMemo } from "react";
7
- import { View, TouchableOpacity, ScrollView, StyleSheet } from "react-native";
7
+ import { View, ScrollView } from "react-native";
8
8
  import { AtomicText } from "@umituz/react-native-design-system/atoms";
9
9
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
10
- import {
11
- FONT_SIZE_MAP,
12
- SUBTITLE_FONT_COLORS,
13
- SUBTITLE_BG_COLORS,
14
- } from "../../infrastructure/constants/subtitle.constants";
10
+ import { Selector, type SelectorItem } from "./generic/Selector";
11
+ import { FONT_SIZE_MAP, SUBTITLE_FONT_COLORS, SUBTITLE_BG_COLORS } from "../../infrastructure/constants/subtitle.constants";
15
12
  import type { SubtitleStyle } from "../../domain/entities/video-project.types";
16
13
 
17
14
  interface SubtitleStylePickerProps {
@@ -24,166 +21,49 @@ interface SubtitleStylePickerProps {
24
21
  const FONT_SIZES: SubtitleStyle["fontSize"][] = ["small", "medium", "large", "extraLarge"];
25
22
  const POSITIONS: SubtitleStyle["position"][] = ["top", "center", "bottom"];
26
23
 
27
- export const SubtitleStylePicker: React.FC<SubtitleStylePickerProps> = ({
28
- style,
29
- previewText,
30
- onChange,
31
- t,
32
- }) => {
24
+ const Section = ({ title, children }: { title: string; children: React.ReactNode }) => {
33
25
  const tokens = useAppDesignTokens();
26
+ return (
27
+ <View style={{ marginTop: tokens.spacing.md }}>
28
+ <AtomicText type="labelSmall" color="textSecondary" style={{ marginBottom: tokens.spacing.sm }}>{title}</AtomicText>
29
+ {children}
30
+ </View>
31
+ );
32
+ };
34
33
 
35
- const styles = useMemo(() => StyleSheet.create({
36
- section: { marginTop: tokens.spacing.md },
37
- sectionLabel: { marginBottom: tokens.spacing.sm },
38
- row: { flexDirection: "row", gap: tokens.spacing.sm, flexWrap: "wrap" },
39
- optionBtn: {
40
- paddingHorizontal: tokens.spacing.md,
41
- paddingVertical: tokens.spacing.sm,
42
- borderRadius: tokens.borders.radius.md,
43
- backgroundColor: tokens.colors.surface,
44
- borderWidth: 1,
45
- borderColor: tokens.colors.border,
46
- },
47
- optionBtnActive: {
48
- backgroundColor: tokens.colors.primaryContainer,
49
- borderColor: tokens.colors.primary,
50
- },
51
- colorRow: { flexDirection: "row", gap: tokens.spacing.sm },
52
- colorBtn: {
53
- width: 36,
54
- height: 36,
55
- borderRadius: 18,
56
- borderWidth: 2,
57
- borderColor: "transparent",
58
- alignItems: "center",
59
- justifyContent: "center",
60
- },
61
- colorBtnActive: {
62
- borderColor: tokens.colors.primary,
63
- },
64
- previewBox: {
65
- height: 72,
66
- backgroundColor: tokens.colors.surfaceVariant,
67
- borderRadius: tokens.borders.radius.md,
68
- marginTop: tokens.spacing.md,
69
- justifyContent: "center",
70
- alignItems: "center",
71
- borderWidth: 1,
72
- borderColor: tokens.colors.border,
73
- overflow: "hidden",
74
- },
75
- previewBubble: {
76
- paddingHorizontal: tokens.spacing.md,
77
- paddingVertical: tokens.spacing.xs,
78
- borderRadius: tokens.borders.radius.sm,
79
- },
80
- }), [tokens]);
34
+ export const SubtitleStylePicker: React.FC<SubtitleStylePickerProps> = ({ style, previewText, onChange, t }) => {
35
+ const tokens = useAppDesignTokens();
81
36
 
82
- const update = (patch: Partial<SubtitleStyle>) => onChange({ ...style, ...patch });
37
+ const fontSizeItems = useMemo<SelectorItem<SubtitleStyle["fontSize"]>[]>(() => FONT_SIZES.map((size) => ({ value: size, label: size })), []);
38
+ const fontColorItems = useMemo<SelectorItem[]>(() => SUBTITLE_FONT_COLORS.map((color) => ({ value: color, label: "", color })), []);
39
+ const bgColorItems = useMemo<SelectorItem[]>(() => SUBTITLE_BG_COLORS.map((bg) => ({ value: bg.value, label: bg.label, color: bg.value })), []);
40
+ const positionItems = useMemo<SelectorItem<SubtitleStyle["position"]>[]>(() => POSITIONS.map((pos) => ({ value: pos, label: pos })), []);
83
41
 
84
42
  return (
85
- <View>
86
- {/* Font size */}
87
- <View style={styles.section}>
88
- <AtomicText type="labelSmall" color="textSecondary" style={styles.sectionLabel}>
89
- {t("subtitle.style.fontSize") || "Size"}
90
- </AtomicText>
91
- <View style={styles.row}>
92
- {FONT_SIZES.map((size) => (
93
- <TouchableOpacity
94
- key={size}
95
- style={[styles.optionBtn, style.fontSize === size && styles.optionBtnActive]}
96
- onPress={() => update({ fontSize: size })}
97
- accessibilityRole="button"
98
- >
99
- <AtomicText
100
- type="labelSmall"
101
- color={style.fontSize === size ? "primary" : "textSecondary"}
102
- >
103
- {size}
104
- </AtomicText>
105
- </TouchableOpacity>
106
- ))}
107
- </View>
108
- </View>
43
+ <ScrollView>
44
+ <Section title={t("subtitle.style.fontSize") || "Size"}>
45
+ <Selector items={fontSizeItems} selectedValue={style.fontSize} onSelect={(value) => onChange({ ...style, fontSize: value })} orientation="horizontal" testID="font-size-selector" />
46
+ </Section>
109
47
 
110
- {/* Font color */}
111
- <View style={styles.section}>
112
- <AtomicText type="labelSmall" color="textSecondary" style={styles.sectionLabel}>
113
- {t("subtitle.style.fontColor") || "Color"}
114
- </AtomicText>
115
- <ScrollView horizontal showsHorizontalScrollIndicator={false}>
116
- <View style={styles.colorRow}>
117
- {SUBTITLE_FONT_COLORS.map((color) => (
118
- <TouchableOpacity
119
- key={color}
120
- style={[styles.colorBtn, { backgroundColor: color }, style.fontColor === color && styles.colorBtnActive]}
121
- onPress={() => update({ fontColor: color })}
122
- accessibilityRole="button"
123
- />
124
- ))}
125
- </View>
126
- </ScrollView>
127
- </View>
48
+ <Section title={t("subtitle.style.fontColor") || "Color"}>
49
+ <Selector items={fontColorItems} selectedValue={style.fontColor} onSelect={(value) => onChange({ ...style, fontColor: value })} orientation="horizontal" colorPreview testID="font-color-selector" />
50
+ </Section>
128
51
 
129
- {/* Background */}
130
- <View style={styles.section}>
131
- <AtomicText type="labelSmall" color="textSecondary" style={styles.sectionLabel}>
132
- {t("subtitle.style.background") || "Background"}
133
- </AtomicText>
134
- <View style={styles.row}>
135
- {SUBTITLE_BG_COLORS.map((bg) => (
136
- <TouchableOpacity
137
- key={bg.value}
138
- style={[styles.optionBtn, style.backgroundColor === bg.value && styles.optionBtnActive]}
139
- onPress={() => update({ backgroundColor: bg.value })}
140
- accessibilityRole="button"
141
- >
142
- <AtomicText
143
- type="labelSmall"
144
- color={style.backgroundColor === bg.value ? "primary" : "textSecondary"}
145
- >
146
- {bg.label}
147
- </AtomicText>
148
- </TouchableOpacity>
149
- ))}
150
- </View>
151
- </View>
52
+ <Section title={t("subtitle.style.background") || "Background"}>
53
+ <Selector items={bgColorItems} selectedValue={style.backgroundColor} onSelect={(value) => onChange({ ...style, backgroundColor: value })} orientation="horizontal" testID="bg-color-selector" />
54
+ </Section>
152
55
 
153
- {/* Position */}
154
- <View style={styles.section}>
155
- <AtomicText type="labelSmall" color="textSecondary" style={styles.sectionLabel}>
156
- {t("subtitle.style.position") || "Position"}
157
- </AtomicText>
158
- <View style={styles.row}>
159
- {POSITIONS.map((pos) => (
160
- <TouchableOpacity
161
- key={pos}
162
- style={[styles.optionBtn, style.position === pos && styles.optionBtnActive]}
163
- onPress={() => update({ position: pos })}
164
- accessibilityRole="button"
165
- >
166
- <AtomicText
167
- type="labelSmall"
168
- color={style.position === pos ? "primary" : "textSecondary"}
169
- >
170
- {pos}
171
- </AtomicText>
172
- </TouchableOpacity>
173
- ))}
174
- </View>
175
- </View>
56
+ <Section title={t("subtitle.style.position") || "Position"}>
57
+ <Selector items={positionItems} selectedValue={style.position} onSelect={(value) => onChange({ ...style, position: value })} testID="position-selector" />
58
+ </Section>
176
59
 
177
- {/* Preview */}
178
- <View style={styles.previewBox}>
179
- <View style={[styles.previewBubble, { backgroundColor: style.backgroundColor }]}>
180
- <AtomicText
181
- style={{ color: style.fontColor, fontSize: FONT_SIZE_MAP[style.fontSize] * 0.75, textAlign: "center" }}
182
- >
60
+ <View style={{ height: 72, backgroundColor: tokens.colors.surfaceVariant, borderRadius: tokens.borders.radius.md, marginTop: tokens.spacing.md, justifyContent: "center", alignItems: "center", borderWidth: 1, borderColor: tokens.colors.border, overflow: "hidden" }}>
61
+ <View style={{ paddingHorizontal: tokens.spacing.md, paddingVertical: tokens.spacing.xs, borderRadius: tokens.borders.radius.sm, backgroundColor: style.backgroundColor }}>
62
+ <AtomicText style={{ color: style.fontColor, fontSize: FONT_SIZE_MAP[style.fontSize] * 0.75, textAlign: "center" }}>
183
63
  {previewText || t("subtitle.preview.placeholder") || "Preview text"}
184
64
  </AtomicText>
185
65
  </View>
186
66
  </View>
187
- </View>
67
+ </ScrollView>
188
68
  );
189
69
  };
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * LayerContent Component
3
3
  * Renders different layer types (text, image, shape)
4
+ * PERFORMANCE: Uses expo-image for better caching and memory management
4
5
  */
5
6
 
6
7
  import React from "react";
7
- import { View, Image, Text as RNText, StyleSheet } from "react-native";
8
+ import { View, Text as RNText, StyleSheet } from "react-native";
9
+ import { Image } from "expo-image";
8
10
  import { AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
11
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
10
12
  import type { Layer, TextLayer, ImageLayer, ShapeLayer } from "../../../domain/entities/video-project.types";
@@ -46,7 +48,10 @@ export const LayerContent: React.FC<LayerContentProps> = ({ layer }) => {
46
48
  <Image
47
49
  source={{ uri: imageLayer.uri }}
48
50
  style={styles.layerImage}
49
- resizeMode="cover"
51
+ contentFit="cover"
52
+ // PERFORMANCE: Cache strategy for better performance
53
+ cachePolicy="memory-disk"
54
+ transition={200} // Smooth fade-in
50
55
  />
51
56
  ) : (
52
57
  <View style={[styles.imagePlaceholder, { backgroundColor: tokens.colors.surface }]}>
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Generic Action Menu Component
3
+ * Single Responsibility: Display action menu with selectable items
4
+ * Replaces: LayerActionsMenu, SceneActionsMenu, SubtitleListPanel actions
5
+ *
6
+ * Features:
7
+ * - Vertical list of actions
8
+ * - Icons + labels
9
+ * - Disabled states
10
+ * - Destructive action styling
11
+ */
12
+
13
+ import React, { useMemo } from "react";
14
+ import { View, TouchableOpacity, StyleSheet } from "react-native";
15
+ import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
16
+ import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
17
+
18
+ export interface ActionMenuItem {
19
+ id: string;
20
+ label: string;
21
+ icon?: string;
22
+ destructive?: boolean;
23
+ disabled?: boolean;
24
+ testID?: string;
25
+ }
26
+
27
+ export interface ActionMenuProps {
28
+ actions: ActionMenuItem[];
29
+ onSelect: (actionId: string) => void;
30
+ testID?: string;
31
+ }
32
+
33
+ /**
34
+ * Generic action menu that can render any set of actions
35
+ * Used for layer actions, scene actions, etc.
36
+ */
37
+ export function ActionMenu({
38
+ actions,
39
+ onSelect,
40
+ testID = "action-menu",
41
+ }: ActionMenuProps) {
42
+ const tokens = useAppDesignTokens();
43
+
44
+ const styles = useMemo(() => StyleSheet.create({
45
+ container: {
46
+ backgroundColor: tokens.colors.surface,
47
+ borderRadius: tokens.borders.radius.lg,
48
+ padding: tokens.spacing.xs,
49
+ },
50
+ actionItem: {
51
+ flexDirection: "row" as const,
52
+ alignItems: "center" as const,
53
+ padding: tokens.spacing.md,
54
+ borderRadius: tokens.borders.radius.md,
55
+ gap: tokens.spacing.md,
56
+ },
57
+ actionItemDestructive: {
58
+ backgroundColor: `${tokens.colors.error}10`,
59
+ },
60
+ actionItemDisabled: {
61
+ opacity: 0.4,
62
+ },
63
+ icon: {
64
+ width: 24,
65
+ height: 24,
66
+ alignItems: "center" as const,
67
+ justifyContent: "center" as const,
68
+ },
69
+ label: {
70
+ flex: 1,
71
+ },
72
+ }), [tokens]);
73
+
74
+ return (
75
+ <View style={styles.container} testID={testID}>
76
+ {actions.map((action) => (
77
+ <TouchableOpacity
78
+ key={action.id}
79
+ style={[
80
+ styles.actionItem,
81
+ action.destructive && styles.actionItemDestructive,
82
+ action.disabled && styles.actionItemDisabled,
83
+ ]}
84
+ onPress={() => onSelect(action.id)}
85
+ disabled={action.disabled}
86
+ testID={action.testID}
87
+ accessibilityRole="button"
88
+ accessibilityLabel={action.label}
89
+ >
90
+ {action.icon && (
91
+ <View style={styles.icon}>
92
+ <AtomicIcon
93
+ name={action.icon}
94
+ size="sm"
95
+ color={action.destructive ? "error" : "textPrimary"}
96
+ />
97
+ </View>
98
+ )}
99
+ <AtomicText
100
+ style={styles.label}
101
+ color={action.destructive ? "error" : "textPrimary"}
102
+ fontWeight="medium"
103
+ >
104
+ {action.label}
105
+ </AtomicText>
106
+ </TouchableOpacity>
107
+ ))}
108
+ </View>
109
+ );
110
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Universal Editor Component
3
+ * Replaces: ImageLayerEditor, TextLayerEditor, ShapeLayerEditor, AudioEditor, AnimationEditor
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
8
+ import { AtomicText } from "@umituz/react-native-design-system/atoms";
9
+ import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
10
+
11
+ export interface EditorSection {
12
+ id: string;
13
+ title?: string;
14
+ component: React.ComponentType<any>;
15
+ props?: Record<string, unknown>;
16
+ }
17
+
18
+ export interface EditorConfig<T = unknown> {
19
+ sections: EditorSection[];
20
+ preview?: React.ComponentType<{ data: T }>;
21
+ actions?: { saveLabel?: string; cancelLabel?: string; onSave: () => void | Promise<void>; onCancel: () => void };
22
+ isValid?: boolean;
23
+ testID?: string;
24
+ }
25
+
26
+ export function Editor<T = unknown>({ sections, preview: Preview, actions, isValid = true, testID = "editor" }: EditorConfig<T>) {
27
+ const tokens = useAppDesignTokens();
28
+ const styles = StyleSheet.create({
29
+ container: { flex: 1, backgroundColor: tokens.colors.surface },
30
+ scrollContent: { padding: tokens.spacing.md, paddingBottom: tokens.spacing.xl },
31
+ section: { marginBottom: tokens.spacing.lg },
32
+ previewContainer: { margin: tokens.spacing.md, borderRadius: tokens.borders.radius.lg, overflow: "hidden", backgroundColor: tokens.colors.surfaceVariant },
33
+ actionsContainer: { padding: tokens.spacing.md, borderTopWidth: 1, borderTopColor: tokens.colors.border },
34
+ });
35
+
36
+ return (
37
+ <View style={styles.container} testID={testID}>
38
+ <ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
39
+ {Preview && <View style={styles.previewContainer}><Preview data={undefined as T} /></View>}
40
+ {sections.map((section) => (
41
+ <View key={section.id} style={styles.section}>
42
+ {section.title && <AtomicText type="labelMedium" color="textSecondary" style={{ marginBottom: tokens.spacing.sm }}>{section.title}</AtomicText>}
43
+ <section.component {...section.props} />
44
+ </View>
45
+ ))}
46
+ </ScrollView>
47
+ {actions && (
48
+ <View style={styles.actionsContainer}>
49
+ <View style={{ flexDirection: "row", gap: tokens.spacing.md }}>
50
+ <TouchableOpacity style={{ flex: 1, paddingVertical: tokens.spacing.md, borderRadius: tokens.borders.radius.md, alignItems: "center", backgroundColor: tokens.colors.surfaceVariant }} onPress={actions.onCancel}>
51
+ <AtomicText fontWeight="medium" color="textPrimary">{actions.cancelLabel || "Cancel"}</AtomicText>
52
+ </TouchableOpacity>
53
+ <TouchableOpacity
54
+ style={{ flex: 1, paddingVertical: tokens.spacing.md, borderRadius: tokens.borders.radius.md, alignItems: "center", backgroundColor: tokens.colors.primary, opacity: isValid ? 1 : 0.4 }}
55
+ onPress={actions.onSave}
56
+ disabled={!isValid}
57
+ >
58
+ <AtomicText fontWeight="semibold" color={isValid ? "onPrimary" : "textSecondary"}>{actions.saveLabel || "Save"}</AtomicText>
59
+ </TouchableOpacity>
60
+ </View>
61
+ </View>
62
+ )}
63
+ </View>
64
+ );
65
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Universal Selector Component
3
+ * Replaces: OptionSelector, FontSizeSelector, TextAlignSelector, ShapeTypeSelector, etc.
4
+ */
5
+
6
+ import React, { useMemo } from "react";
7
+ import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
8
+ import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
+ import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
10
+
11
+ export interface SelectorItem<T = string> {
12
+ value: T;
13
+ label: string;
14
+ icon?: string;
15
+ color?: string;
16
+ disabled?: boolean;
17
+ }
18
+
19
+ export interface SelectorProps<T = string> {
20
+ items: SelectorItem<T>[];
21
+ selectedValue: T;
22
+ onSelect: (value: T) => void;
23
+ orientation?: "horizontal" | "vertical" | "grid";
24
+ itemWidth?: number;
25
+ itemHeight?: number;
26
+ icon?: boolean;
27
+ colorPreview?: boolean;
28
+ testID?: string;
29
+ }
30
+
31
+ const SelectorItem = React.memo<{ item: SelectorItem; isSelected: boolean; onSelect: () => void; styles: any; icon: boolean; colorPreview: boolean }>(
32
+ ({ item, isSelected, onSelect, styles, icon, colorPreview }) => (
33
+ <TouchableOpacity
34
+ style={[styles.item, isSelected && styles.itemSelected, item.disabled && styles.itemDisabled]}
35
+ onPress={onSelect}
36
+ disabled={item.disabled}
37
+ accessibilityRole="button"
38
+ accessibilityState={{ selected: isSelected }}
39
+ accessibilityLabel={item.label}
40
+ >
41
+ {colorPreview && item.color && <View style={[styles.colorPreview, { backgroundColor: item.color }]} />}
42
+ {icon && item.icon && <AtomicIcon name={item.icon} size="sm" color="textPrimary" />}
43
+ <AtomicText type="labelSmall" style={styles.label} color={isSelected ? "primary" : "textPrimary"}>{item.label}</AtomicText>
44
+ </TouchableOpacity>
45
+ )
46
+ );
47
+
48
+ SelectorItem.displayName = "SelectorItem";
49
+
50
+ export function Selector<T = string>({
51
+ items,
52
+ selectedValue,
53
+ onSelect,
54
+ orientation = "horizontal",
55
+ itemWidth = 80,
56
+ itemHeight = 40,
57
+ icon = false,
58
+ colorPreview = false,
59
+ testID = "selector",
60
+ }: SelectorProps<T>) {
61
+ const tokens = useAppDesignTokens();
62
+ const styles = useMemo(() => StyleSheet.create({
63
+ container: { gap: tokens.spacing.sm },
64
+ scrollContent: { gap: tokens.spacing.sm, paddingHorizontal: orientation === "horizontal" ? tokens.spacing.md : 0 },
65
+ gridContainer: { flexDirection: "row", flexWrap: "wrap", gap: tokens.spacing.sm },
66
+ item: { width: orientation === "grid" ? undefined : itemWidth, height: itemHeight, borderRadius: tokens.borders.radius.md, borderWidth: 1, borderColor: tokens.colors.border, backgroundColor: tokens.colors.surface, alignItems: "center", justifyContent: "center", paddingHorizontal: tokens.spacing.md },
67
+ itemSelected: { borderColor: tokens.colors.primary, backgroundColor: `${tokens.colors.primary}20` },
68
+ itemDisabled: { opacity: 0.4 },
69
+ label: { color: tokens.colors.textPrimary },
70
+ colorPreview: { width: 24, height: 24, borderRadius: 12 },
71
+ }), [tokens, orientation, itemWidth, itemHeight]);
72
+
73
+ const content = items.map((item) => (
74
+ <SelectorItem
75
+ key={String(item.value)}
76
+ item={item}
77
+ isSelected={item.value === selectedValue}
78
+ onSelect={() => onSelect(item.value)}
79
+ styles={styles}
80
+ icon={icon}
81
+ colorPreview={colorPreview}
82
+ />
83
+ ));
84
+
85
+ return (
86
+ <View style={styles.container} testID={testID}>
87
+ {orientation === "horizontal" ? (
88
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
89
+ {content}
90
+ </ScrollView>
91
+ ) : (
92
+ <View style={styles.gridContainer}>{content}</View>
93
+ )}
94
+ </View>
95
+ );
96
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Generic Toolbar Component
3
+ * Replaces: EditorToolPanel and similar toolbar patterns
4
+ */
5
+
6
+ import React, { useMemo } from "react";
7
+ import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
8
+ import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
+ import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
10
+
11
+ export interface ToolbarButton {
12
+ id: string;
13
+ icon: string;
14
+ label: string;
15
+ onPress: () => void;
16
+ disabled?: boolean;
17
+ destructive?: boolean;
18
+ showBadge?: boolean;
19
+ badgeColor?: string;
20
+ testID?: string;
21
+ }
22
+
23
+ export interface ToolbarSection {
24
+ id: string;
25
+ buttons: ToolbarButton[];
26
+ }
27
+
28
+ export interface ToolbarProps {
29
+ sections: ToolbarSection[];
30
+ orientation?: "horizontal" | "vertical";
31
+ scrollable?: boolean;
32
+ testID?: string;
33
+ }
34
+
35
+ const ToolbarButtonComp = React.memo<{ button: ToolbarButton; styles: any }>(({ button, styles }) => (
36
+ <TouchableOpacity
37
+ style={[styles.button, button.destructive && styles.buttonDestructive, button.disabled && styles.buttonDisabled]}
38
+ onPress={button.onPress}
39
+ disabled={button.disabled}
40
+ testID={button.testID}
41
+ accessibilityRole="button"
42
+ accessibilityLabel={button.label}
43
+ >
44
+ <AtomicIcon name={button.icon as any} size="sm" color={button.destructive ? "error" : "primary"} />
45
+ <AtomicText type="labelSmall" style={styles.label}>{button.label}</AtomicText>
46
+ {button.showBadge && <View style={[styles.badge, { backgroundColor: button.badgeColor }]}><AtomicText style={styles.badgeText}>!</AtomicText></View>}
47
+ </TouchableOpacity>
48
+ ));
49
+
50
+ ToolbarButtonComp.displayName = "ToolbarButton";
51
+
52
+ export function Toolbar({ sections, orientation = "horizontal", scrollable = false, testID = "toolbar" }: ToolbarProps) {
53
+ const tokens = useAppDesignTokens();
54
+ const styles = useMemo(() => StyleSheet.create({
55
+ container: { backgroundColor: tokens.colors.surface, borderRadius: tokens.borders.radius.lg, padding: tokens.spacing.sm },
56
+ scrollContent: { gap: tokens.spacing.sm, paddingHorizontal: tokens.spacing.sm },
57
+ sectionContainer: { gap: tokens.spacing.sm },
58
+ button: { alignItems: "center", padding: tokens.spacing.sm, borderRadius: tokens.borders.radius.md, gap: tokens.spacing.xs, backgroundColor: tokens.colors.surfaceVariant, borderWidth: 1, borderColor: tokens.colors.border, minWidth: orientation === "horizontal" ? 60 : 50 },
59
+ buttonDestructive: { backgroundColor: `${tokens.colors.error}10`, borderColor: tokens.colors.error },
60
+ buttonDisabled: { opacity: 0.4 },
61
+ label: { color: tokens.colors.textPrimary },
62
+ badge: { position: "absolute", top: -4, right: -4, minWidth: 16, height: 16, borderRadius: 8, borderWidth: 2, borderColor: tokens.colors.surface, alignItems: "center", justifyContent: "center" },
63
+ badgeText: { color: tokens.colors.onPrimary, fontSize: 10, fontWeight: "bold" },
64
+ }), [tokens, orientation]);
65
+
66
+ const content = sections.map((section) => (
67
+ <View key={section.id} style={styles.sectionContainer}>
68
+ {section.buttons.map((button) => <ToolbarButtonComp key={button.id} button={button} styles={styles} />)}
69
+ </View>
70
+ ));
71
+
72
+ return (
73
+ <View style={styles.container} testID={testID}>
74
+ {scrollable ? <ScrollView horizontal={orientation === "horizontal"} showsHorizontalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>{content}</ScrollView> : content}
75
+ </View>
76
+ );
77
+ }
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * ImagePreview Component
3
3
  * Image preview or placeholder for image layer editor
4
+ * PERFORMANCE: Uses expo-image with caching for better performance
4
5
  */
5
6
 
6
7
  import React from "react";
7
- import { View, Image, StyleSheet } from "react-native";
8
+ import { View, StyleSheet } from "react-native";
9
+ import { Image } from "expo-image";
8
10
  import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
11
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
10
12
 
@@ -24,6 +26,10 @@ export const ImagePreview: React.FC<ImagePreviewProps> = ({
24
26
  <Image
25
27
  source={{ uri: imageUri }}
26
28
  style={[styles.imagePreview, { opacity }]}
29
+ // PERFORMANCE: Cache strategy for better performance
30
+ cachePolicy="memory-disk"
31
+ transition={200} // Smooth fade-in
32
+ placeholder="#F0F0F0" // Placeholder color while loading
27
33
  />
28
34
  );
29
35
  }