@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
@@ -5,13 +5,13 @@
5
5
  */
6
6
 
7
7
  import React, { memo } from "react";
8
- import { View, StyleSheet, TouchableOpacity } from "react-native";
8
+ import { View, StyleSheet } from "react-native";
9
9
  import { GestureDetector } from "react-native-gesture-handler";
10
10
  import { Image } from "expo-image";
11
- import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
12
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
+ import { AtomicText } from "@umituz/react-native-design-system/atoms";
12
+ import { useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system/theme";
13
13
  import { useTransformGesture } from "../../infrastructure/gesture/useTransformGesture";
14
- import { Layer, isTextLayer, isStickerLayer } from "../../domain/entities/Layer";
14
+ import type { Layer } from "../entities/Layer.entity".entity";
15
15
 
16
16
  interface DraggableLayerProps {
17
17
  layer: Layer;
@@ -30,15 +30,15 @@ export const DraggableLayer = memo<DraggableLayerProps>(({
30
30
  onTransformEnd,
31
31
  }) => {
32
32
  const tokens = useAppDesignTokens();
33
- const { state, gestures } = useTransformGesture(layer, {
34
- onTransformEnd,
35
- onPress,
36
- });
33
+ const { state, gestures } = useTransformGesture(
34
+ { x: layer.x, y: layer.y, scale: layer.scale, rotation: layer.rotation },
35
+ { onTransformEnd, onPress }
36
+ );
37
37
 
38
38
  return (
39
39
  <GestureDetector gesture={gestures.composed}>
40
40
  <View
41
- accessibilityLabel={isTextLayer(layer) ? layer.text || "Text layer" : "Sticker layer"}
41
+ accessibilityLabel={layer.isText() ? layer.text || "Text layer" : "Sticker layer"}
42
42
  accessibilityRole="button"
43
43
  style={[
44
44
  styles.container,
@@ -63,12 +63,12 @@ export const DraggableLayer = memo<DraggableLayerProps>(({
63
63
  borderStyle: "dashed",
64
64
  backgroundColor: isSelected
65
65
  ? tokens.colors.primary + "10"
66
- : isTextLayer(layer)
66
+ : layer.isText()
67
67
  ? layer.backgroundColor
68
68
  : "transparent",
69
69
  }}
70
70
  >
71
- {isTextLayer(layer) ? (
71
+ {layer.isText() ? (
72
72
  <AtomicText
73
73
  style={{
74
74
  fontSize: layer.fontSize,
@@ -81,7 +81,7 @@ export const DraggableLayer = memo<DraggableLayerProps>(({
81
81
  >
82
82
  {layer.text || "TAP TO EDIT"}
83
83
  </AtomicText>
84
- ) : isStickerLayer(layer) ? (
84
+ ) : layer.isSticker() ? (
85
85
  renderStickerContent(layer.uri, tokens)
86
86
  ) : null}
87
87
  </View>
@@ -92,7 +92,7 @@ export const DraggableLayer = memo<DraggableLayerProps>(({
92
92
 
93
93
  DraggableLayer.displayName = "DraggableLayer";
94
94
 
95
- function renderStickerContent(uri: string, tokens: any) {
95
+ function renderStickerContent(uri: string, _tokens: DesignTokens) {
96
96
  const isEmoji = isEmojiString(uri);
97
97
 
98
98
  if (isEmoji) {
@@ -4,20 +4,20 @@
4
4
  */
5
5
 
6
6
  import React, { memo } from "react";
7
- import { View, StyleSheet } from "react-native";
7
+ import { View, StyleSheet, type ViewStyle } from "react-native";
8
8
  import { Image } from "expo-image";
9
9
  import { DraggableLayer } from "./DraggableLayer";
10
- import { Layer } from "../../domain/entities/Layer";
11
- import type { FilterValues } from "../../domain/entities/Filters";
10
+ import type { Layer } from "../entities/Layer.entity".entity";
11
+ import type { FilterSettings } from "../../domain/value-objects/FilterSettings.vo";
12
12
 
13
13
  interface EditorCanvasProps {
14
14
  imageUrl: string;
15
15
  layers: Layer[];
16
16
  activeLayerId: string | null;
17
- filters: FilterValues;
17
+ filters: FilterSettings;
18
18
  onLayerTap: (layerId: string) => void;
19
19
  onLayerTransform: (layerId: string, transform: { x: number; y: number; scale: number; rotation: number }) => void;
20
- style?: any;
20
+ style?: ViewStyle;
21
21
  }
22
22
 
23
23
  export const EditorCanvas = memo<EditorCanvasProps>(({
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Editor Content Component
3
+ * Scrollable content area with canvas and tools
4
+ */
5
+
6
+ import React from "react";
7
+ import { ScrollView } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
9
+ import { EditorCanvas } from "./EditorCanvas";
10
+ import { FontControls } from "./FontControls";
11
+ import type { Layer } from "../entities/Layer.entity".entity";
12
+ import type { FilterSettings } from "../../domain/value-objects/FilterSettings.vo";
13
+ import type { EditorUIState } from "../../application/hooks/useEditorUI";
14
+
15
+ interface EditorContentProps {
16
+ imageUrl: string;
17
+ layers: Layer[];
18
+ filters: FilterSettings;
19
+ activeLayerId: string | null;
20
+ selectedFont: string;
21
+ fontSize: number;
22
+ fonts: readonly string[];
23
+ customTools?: React.ReactNode | ((ui: EditorUIState) => React.ReactNode);
24
+ ui: EditorUIState;
25
+ onFontSizeChange: (size: number) => void;
26
+ onFontSelect: (font: string) => void;
27
+ }
28
+
29
+ export function EditorContent({
30
+ imageUrl,
31
+ layers,
32
+ filters,
33
+ activeLayerId,
34
+ selectedFont,
35
+ fontSize,
36
+ fonts,
37
+ customTools,
38
+ ui,
39
+ onFontSizeChange,
40
+ onFontSelect,
41
+ }: EditorContentProps) {
42
+ const tokens = useAppDesignTokens();
43
+
44
+ return (
45
+ <ScrollView
46
+ contentContainerStyle={{
47
+ padding: tokens.spacing.md,
48
+ gap: tokens.spacing.md,
49
+ }}
50
+ >
51
+ <EditorCanvas
52
+ imageUrl={imageUrl}
53
+ layers={layers}
54
+ activeLayerId={activeLayerId}
55
+ filters={filters}
56
+ onLayerTap={ui.handleTextLayerTap}
57
+ onLayerTransform={ui.handleLayerTransform}
58
+ />
59
+
60
+ {typeof customTools === "function" ? customTools(ui) : customTools}
61
+
62
+ <FontControls
63
+ fontSize={fontSize}
64
+ selectedFont={selectedFont}
65
+ fonts={fonts}
66
+ onFontSizeChange={onFontSizeChange}
67
+ onFontSelect={onFontSelect}
68
+ />
69
+ </ScrollView>
70
+ );
71
+ }
72
+
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Editor Header Component
3
+ * Header with close, title, and save buttons
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity } 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
+ interface EditorHeaderProps {
12
+ title: string;
13
+ saveLabel: string;
14
+ onClose: () => void;
15
+ onSave: () => void;
16
+ }
17
+
18
+ export function EditorHeader({ title, saveLabel, onClose, onSave }: EditorHeaderProps) {
19
+ const tokens = useAppDesignTokens();
20
+
21
+ return (
22
+ <View
23
+ style={{
24
+ flexDirection: "row",
25
+ alignItems: "center",
26
+ justifyContent: "space-between",
27
+ paddingHorizontal: tokens.spacing.md,
28
+ paddingVertical: tokens.spacing.sm,
29
+ borderBottomWidth: 1,
30
+ borderBottomColor: tokens.colors.border,
31
+ }}
32
+ >
33
+ <TouchableOpacity onPress={onClose} accessibilityLabel="Close editor" accessibilityRole="button">
34
+ <AtomicIcon name="close" size="md" color="textPrimary" />
35
+ </TouchableOpacity>
36
+
37
+ <AtomicText type="headlineSmall" style={{ flex: 1, textAlign: "center" as const }}>
38
+ {title}
39
+ </AtomicText>
40
+
41
+ <TouchableOpacity onPress={onSave} accessibilityLabel="Save" accessibilityRole="button">
42
+ <AtomicText fontWeight="bold" color="primary">
43
+ {saveLabel}
44
+ </AtomicText>
45
+ </TouchableOpacity>
46
+ </View>
47
+ );
48
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Editor Sheets Component
3
+ * All bottom sheets for the editor
4
+ */
5
+
6
+ import React from "react";
7
+ import { BottomSheetModal } from "@umituz/react-native-design-system/molecules";
8
+ import { TextEditorSheet } from "./sheets/TextEditorSheet";
9
+ import { StickerPicker } from "./sheets/StickerPicker";
10
+ import { FilterSheet } from "./sheets/FilterSheet";
11
+ import { AdjustmentsSheet } from "./sheets/AdjustmentsSheet";
12
+ import { LayerManager } from "./sheets/LayerManager";
13
+ import { AIMagicSheet } from "./sheets/AIMagicSheet";
14
+ import type { FilterOption } from "./sheets/FilterSheet";
15
+ import type { FilterSettings } from "../../domain/value-objects/FilterSettings.vo";
16
+ import type { EditorUIState } from "../../application/hooks/useEditorUI";
17
+
18
+ interface EditorSheetsProps {
19
+ ui: EditorUIState;
20
+ filters: FilterSettings;
21
+ t: (key: string) => string;
22
+ onAICaption?: (style: string) => Promise<string> | void;
23
+ }
24
+
25
+ export function EditorSheets({ ui, filters, t, onAICaption }: EditorSheetsProps) {
26
+ return (
27
+ <>
28
+ <BottomSheetModal ref={ui.textEditorSheetRef} snapPoints={["55%"]}>
29
+ <TextEditorSheet
30
+ value={ui.editingText}
31
+ onChange={ui.setEditingText}
32
+ onSave={ui.handleSaveText}
33
+ t={t}
34
+ color={ui.editingColor}
35
+ onColorChange={ui.setEditingColor}
36
+ textAlign={ui.editingAlign}
37
+ onTextAlignChange={ui.setEditingAlign}
38
+ isBold={ui.editingBold}
39
+ onBoldChange={ui.setEditingBold}
40
+ isItalic={ui.editingItalic}
41
+ onItalicChange={ui.setEditingItalic}
42
+ />
43
+ </BottomSheetModal>
44
+
45
+ <BottomSheetModal ref={ui.stickerSheetRef} snapPoints={["50%"]}>
46
+ <StickerPicker onSelectSticker={ui.handleSelectSticker} />
47
+ </BottomSheetModal>
48
+
49
+ <BottomSheetModal ref={ui.filterSheetRef} snapPoints={["40%"]}>
50
+ <FilterSheet
51
+ selectedFilter={ui.selectedFilter}
52
+ onSelectFilter={(option: FilterOption) => {
53
+ ui.setSelectedFilter(option.id);
54
+ ui.updateFilters(option.filters);
55
+ ui.filterSheetRef.current?.dismiss();
56
+ }}
57
+ />
58
+ </BottomSheetModal>
59
+
60
+ <BottomSheetModal ref={ui.adjustmentsSheetRef} snapPoints={["55%"]}>
61
+ <AdjustmentsSheet filters={filters} onFiltersChange={ui.updateFilters} />
62
+ </BottomSheetModal>
63
+
64
+ <BottomSheetModal ref={ui.layerSheetRef} snapPoints={["55%"]}>
65
+ <LayerManager
66
+ layers={ui.layers}
67
+ activeLayerId={ui.activeLayerId}
68
+ onSelectLayer={ui.selectLayer}
69
+ onDeleteLayer={ui.deleteLayer}
70
+ onDuplicateLayer={ui.duplicateLayer}
71
+ onMoveLayerUp={ui.moveLayerUp}
72
+ onMoveLayerDown={ui.moveLayerDown}
73
+ t={t}
74
+ />
75
+ </BottomSheetModal>
76
+
77
+ {onAICaption && (
78
+ <BottomSheetModal ref={ui.aiSheetRef} snapPoints={["60%"]}>
79
+ <AIMagicSheet onGenerateCaption={onAICaption} />
80
+ </BottomSheetModal>
81
+ )}
82
+ </>
83
+ );
84
+ }
85
+
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import React, { memo } from "react";
7
- import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
7
+ import { View, ScrollView, TouchableOpacity, StyleSheet, type ViewStyle } 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
10
  import { Slider } from "./ui/Slider";
@@ -23,7 +23,7 @@ interface FontControlsProps {
23
23
  fonts?: readonly string[];
24
24
  onFontSizeChange: (size: number) => void;
25
25
  onFontSelect: (font: string) => void;
26
- style?: any;
26
+ style?: ViewStyle;
27
27
  }
28
28
 
29
29
  export const FontControls = memo<FontControlsProps>(({
@@ -8,11 +8,11 @@ import { View, TouchableOpacity } from "react-native";
8
8
  import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
9
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
10
10
  import { Slider } from "../ui/Slider";
11
- import { DEFAULT_FILTERS } from "../../../domain/entities/Filters";
11
+ import { DEFAULT_FILTERS, type FilterValues } from "../../../domain/entities/Filters";
12
12
 
13
13
  interface AdjustmentsSheetProps {
14
- filters: Record<string, number>;
15
- onFiltersChange: (filters: Record<string, number>) => void;
14
+ filters: FilterValues;
15
+ onFiltersChange: (filters: FilterValues) => void;
16
16
  }
17
17
 
18
18
  export const AdjustmentsSheet = memo<AdjustmentsSheetProps>(({
@@ -21,7 +21,7 @@ export const AdjustmentsSheet = memo<AdjustmentsSheetProps>(({
21
21
  }) => {
22
22
  const tokens = useAppDesignTokens();
23
23
 
24
- const update = (key: string, val: number) => {
24
+ const update = (key: keyof FilterValues, val: number) => {
25
25
  onFiltersChange({ ...filters, [key]: val });
26
26
  };
27
27
 
@@ -21,7 +21,7 @@ interface FilterSheetProps {
21
21
  filters?: FilterOption[];
22
22
  }
23
23
 
24
- const DEFAULT_FILTERS: FilterOption[] = [
24
+ export const DEFAULT_FILTERS: FilterOption[] = [
25
25
  {
26
26
  id: "none",
27
27
  name: "None",
@@ -7,7 +7,7 @@ import React, { memo } from "react";
7
7
  import { View, ScrollView, TouchableOpacity } from "react-native";
8
8
  import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
9
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
10
- import { Layer, isTextLayer } from "../../../domain/entities/Layer";
10
+ import { Layer } from "../entities/Layer.entity"";
11
11
 
12
12
  interface LayerManagerProps {
13
13
  layers: Layer[];
@@ -47,7 +47,7 @@ export const LayerManager = memo<LayerManagerProps>(({
47
47
  ) : (
48
48
  sortedLayers.map((layer, idx) => {
49
49
  const isActive = activeLayerId === layer.id;
50
- const label = isTextLayer(layer)
50
+ const label = layer.isText()
51
51
  ? layer.text || t("photo_editor.untitled") || "Untitled"
52
52
  : "Sticker";
53
53
  const isTop = idx === 0;
@@ -60,12 +60,11 @@ export const LayerManager = memo<LayerManagerProps>(({
60
60
  flexDirection: "row",
61
61
  alignItems: "center",
62
62
  padding: tokens.spacing.sm,
63
- backgroundColor: tokens.colors.surfaceVariant,
63
+ backgroundColor: isActive ? tokens.colors.primary + "10" : tokens.colors.surfaceVariant,
64
64
  borderRadius: tokens.borders.radius.md,
65
65
  marginBottom: tokens.spacing.xs,
66
66
  borderWidth: 2,
67
67
  borderColor: isActive ? tokens.colors.primary : "transparent",
68
- backgroundColor: isActive ? tokens.colors.primary + "10" : tokens.colors.surfaceVariant,
69
68
  }}
70
69
  onPress={() => onSelectLayer(layer.id)}
71
70
  accessibilityLabel={`${layer.type} layer: ${label}`}
@@ -8,7 +8,7 @@ import { View, TextInput, TouchableOpacity, StyleSheet } from "react-native";
8
8
  import { AtomicText, AtomicButton } from "@umituz/react-native-design-system/atoms";
9
9
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
10
10
  import { ColorPicker } from "../ui/ColorPicker";
11
- import type { TextAlign } from "../../../domain/entities/Layer";
11
+ import type { TextAlign } from "../entities/Layer.entity"";
12
12
 
13
13
  interface TextEditorSheetProps {
14
14
  value: string;
package/src/types.ts CHANGED
@@ -1,25 +1,15 @@
1
1
  /**
2
- * Legacy Types (for backward compatibility)
3
- *
4
- * @deprecated Use types from src/domain/entities/ instead
2
+ * Central Type Exports
3
+ * Re-exports domain types for convenience
5
4
  */
6
5
 
7
- export type { TextAlign } from "./domain/entities/Layer";
8
- export type { Layer, TextLayer, StickerLayer } from "./domain/entities/Layer";
6
+ export type { TextAlign } from "../entities/Layer.entity"";
7
+ export type { Layer, TextLayer, StickerLayer, TextLayerData, StickerLayerData } from "../entities/Layer.entity"";
9
8
  export type { Transform } from "./domain/entities/Transform";
10
- export type { FilterValues as ImageFilters, DEFAULT_FILTERS as DEFAULT_IMAGE_FILTERS } from "./domain/entities/Filters";
9
+ export type { FilterValues as ImageFilters } from "./domain/entities/Filters";
11
10
 
12
11
  // Re-export type guards
13
- export { isTextLayer, isStickerLayer } from "./domain/entities/Layer";
12
+ export { isTextLayer, isStickerLayer } from "../entities/Layer.entity"";
14
13
 
15
- // Legacy type alias
16
- export interface TextLayerData extends TextLayer {}
17
- export interface StickerLayerData extends StickerLayer {}
18
-
19
- // Legacy EditorState (kept for compatibility)
20
- export interface EditorState {
21
- layers: Layer[];
22
- activeLayerId: string | null;
23
- canvasSize: { width: number; height: number };
24
- filters: ImageFilters;
25
- }
14
+ // Re-export DEFAULT_FILTERS as value (not type)
15
+ export { DEFAULT_FILTERS as DEFAULT_IMAGE_FILTERS } from "./domain/entities/Filters";
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Constants Utility
3
+ * Shared constants for the editor
4
+ */
5
+
6
+ export const DEFAULT_COLORS = [
7
+ "#FFFFFF", "#000000", "#888888", "#CCCCCC",
8
+ "#FF3B30", "#FF9500", "#FFCC00", "#FF2D55",
9
+ "#34C759", "#30B0C7", "#007AFF", "#5AC8FA",
10
+ "#5856D6", "#AF52DE", "#FF6B6B", "#FFD93D",
11
+ "#6BCB77", "#4D96FF", "#C77DFF", "#F72585",
12
+ ] as const;
13
+
14
+ export const DEFAULT_FONTS = [
15
+ "System",
16
+ "Impact",
17
+ "Comic",
18
+ "Serif",
19
+ "Retro",
20
+ ] as const;
21
+
22
+ export const DEFAULT_STICKERS = [
23
+ "😀", "😂", "🤣", "😍", "🥰", "😎", "🤯", "🥳", "😤", "💀",
24
+ "🔥", "❤️", "💯", "✨", "🎉", "🤡", "👀", "🙌", "👏", "💪",
25
+ "🤝", "🙈", "🐶", "🐱", "🦊", "🐸", "🌟", "⭐", "🌈", "☀️",
26
+ "🌙", "💫",
27
+ ] as const;
28
+
29
+ export const AI_STYLES = [
30
+ { id: "viral", label: "✨ Viral", desc: "Catchy & shareable" },
31
+ { id: "funny", label: "😂 Funny", desc: "Humor that connects" },
32
+ { id: "savage", label: "🔥 Savage", desc: "Bold & edgy" },
33
+ { id: "wholesome", label: "💕 Wholesome", desc: "Warm & positive" },
34
+ { id: "sarcastic", label: "😏 Sarcastic", desc: "Witty & ironic" },
35
+ { id: "relatable", label: "🎯 Relatable", desc: "Everyone gets it" },
36
+ ] as const;
37
+
38
+ export const FILTER_PRESETS = [
39
+ {
40
+ id: "none",
41
+ name: "None",
42
+ icon: "close",
43
+ filters: { brightness: 1, contrast: 1, saturation: 1, sepia: 0, grayscale: 0 },
44
+ },
45
+ {
46
+ id: "sepia",
47
+ name: "Sepia",
48
+ icon: "brush",
49
+ filters: { sepia: 0.7, saturation: 0.8 },
50
+ },
51
+ {
52
+ id: "grayscale",
53
+ name: "B&W",
54
+ icon: "swap-horizontal",
55
+ filters: { grayscale: 1, saturation: 0 },
56
+ },
57
+ {
58
+ id: "vintage",
59
+ name: "Vintage",
60
+ icon: "flash",
61
+ filters: { sepia: 0.3, contrast: 1.1, brightness: 0.9 },
62
+ },
63
+ {
64
+ id: "warm",
65
+ name: "Warm",
66
+ icon: "sparkles",
67
+ filters: { brightness: 1.05, saturation: 1.2 },
68
+ },
69
+ {
70
+ id: "cool",
71
+ name: "Cool",
72
+ icon: "image",
73
+ filters: { contrast: 1.05, brightness: 1.02, saturation: 0.85 },
74
+ },
75
+ ] as const;
76
+
77
+ export const SLIDER_CONFIGS = {
78
+ brightness: { min: 0.5, max: 2, step: 0.05, default: 1 },
79
+ contrast: { min: 0.5, max: 2, step: 0.05, default: 1 },
80
+ saturation: { min: 0, max: 2, step: 0.05, default: 1 },
81
+ hueRotate: { min: 0, max: 360, step: 1, default: 0 },
82
+ sepia: { min: 0, max: 1, step: 0.05, default: 0 },
83
+ grayscale: { min: 0, max: 1, step: 0.05, default: 0 },
84
+ } as const;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Formatters Utility
3
+ * Value formatting for UI display
4
+ */
5
+
6
+ export function formatPercentage(value: number): string {
7
+ const rounded = (value - 1) * 100;
8
+ return `${rounded >= 0 ? "+" : ""}${Math.round(rounded)}%`;
9
+ }
10
+
11
+ export function formatDegrees(value: number): string {
12
+ return `${Math.round(value)}°`;
13
+ }
14
+
15
+ export function formatSliderValue(
16
+ value: number,
17
+ type: "percentage" | "degrees" | "integer" = "percentage"
18
+ ): string {
19
+ switch (type) {
20
+ case "percentage":
21
+ return formatPercentage(value);
22
+ case "degrees":
23
+ return formatDegrees(value);
24
+ case "integer":
25
+ return Math.round(value).toString();
26
+ default:
27
+ return value.toString();
28
+ }
29
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Helpers Utility
3
+ * Common helper functions
4
+ */
5
+
6
+ import type { FilterData } from "../domain/value-objects/FilterSettings.vo";
7
+
8
+ export function createBrightnessOverlay(brightness: number): {
9
+ color: string;
10
+ opacity: number;
11
+ } | null {
12
+ if (brightness < 1) {
13
+ return { color: "black", opacity: Math.min(0.6, 1 - brightness) };
14
+ }
15
+ if (brightness > 1) {
16
+ return { color: "white", opacity: Math.min(0.4, brightness - 1) };
17
+ }
18
+ return null;
19
+ }
20
+
21
+ export function mergeFilters(
22
+ base: FilterData,
23
+ updates: Partial<FilterData>
24
+ ): FilterData {
25
+ return {
26
+ ...base,
27
+ ...updates,
28
+ };
29
+ }
30
+
31
+ export function generateId(prefix: string): string {
32
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
33
+ }
34
+
35
+ export function sortByZIndex<T extends { zIndex: number }>(
36
+ items: T[]
37
+ ): T[] {
38
+ return [...items].sort((a, b) => a.zIndex - b.zIndex);
39
+ }
40
+
41
+ export function getNextZIndex(items: { zIndex: number }[]): number {
42
+ return items.length > 0 ? Math.max(...items.map((item) => item.zIndex)) + 1 : 0;
43
+ }
44
+
45
+ export function capitalizeFirst(str: string): string {
46
+ return str.charAt(0).toUpperCase() + str.slice(1);
47
+ }
48
+
49
+ export function truncateText(text: string, maxLength: number): string {
50
+ return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
51
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Utils Export
3
+ * Central export for all utility functions
4
+ */
5
+
6
+ export * from "./formatters";
7
+ export * from "./validators";
8
+ export * from "./constants";
9
+ export * from "./helpers";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Validators Utility
3
+ * Input validation for editor operations
4
+ */
5
+
6
+ export function isValidColor(color: string): boolean {
7
+ return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color);
8
+ }
9
+
10
+ export function isValidUrl(url: string): boolean {
11
+ try {
12
+ new URL(url);
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ export function isEmojiString(str: string): boolean {
20
+ return (
21
+ str.length <= 4 &&
22
+ !/^https?:\/\//i.test(str) &&
23
+ !str.startsWith("/") &&
24
+ /^[\p{Emoji}\p{Emoji_Component}]+$/u.test(str)
25
+ );
26
+ }
27
+
28
+ export function clamp(value: number, min: number, max: number): number {
29
+ return Math.min(Math.max(value, min), max);
30
+ }
31
+
32
+ export function validateFontSize(size: number): number {
33
+ return clamp(size, 8, 120);
34
+ }
35
+
36
+ export function validateOpacity(opacity: number): number {
37
+ return clamp(opacity, 0, 1);
38
+ }