@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,181 +0,0 @@
1
- import React, { useState, useRef, useCallback, useEffect } from "react";
2
- import { View, StyleSheet } 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
- export interface LayerTransform {
8
- x: number;
9
- y: number;
10
- scale: number;
11
- rotation: number;
12
- }
13
-
14
- interface DraggableTextProps {
15
- text: string;
16
- color: string;
17
- fontSize?: number;
18
- fontFamily?: string;
19
- initialX: number;
20
- initialY: number;
21
- rotation?: number;
22
- scale?: number;
23
- opacity?: number;
24
- textAlign?: "center" | "left" | "right";
25
- backgroundColor?: string;
26
- isBold?: boolean;
27
- isItalic?: boolean;
28
- onTransformEnd: (transform: LayerTransform) => void;
29
- onPress: () => void;
30
- isSelected?: boolean;
31
- }
32
-
33
- export const DraggableText: React.FC<DraggableTextProps> = ({
34
- text,
35
- color,
36
- fontSize = 24,
37
- fontFamily = "System",
38
- initialX,
39
- initialY,
40
- rotation: rotationProp = 0,
41
- scale: scaleProp = 1,
42
- opacity = 1,
43
- textAlign = "center",
44
- backgroundColor = "transparent",
45
- isBold = false,
46
- isItalic = false,
47
- onTransformEnd,
48
- onPress,
49
- isSelected,
50
- }) => {
51
- const tokens = useAppDesignTokens();
52
- const [position, setPosition] = useState({ x: initialX, y: initialY });
53
- const [scale, setScale] = useState(scaleProp);
54
- const [rotation, setRotation] = useState(rotationProp); // degrees
55
-
56
- // Sync when props change (e.g., undo/redo)
57
- useEffect(() => { setPosition({ x: initialX, y: initialY }); }, [initialX, initialY]);
58
- useEffect(() => { setScale(scaleProp); }, [scaleProp]);
59
- useEffect(() => { setRotation(rotationProp); }, [rotationProp]);
60
-
61
- // Refs for gesture callbacks to avoid stale closures
62
- const positionRef = useRef(position);
63
- positionRef.current = position;
64
- const scaleRef = useRef(scale);
65
- scaleRef.current = scale;
66
- const rotationRef = useRef(rotation);
67
- rotationRef.current = rotation;
68
- const onTransformEndRef = useRef(onTransformEnd);
69
- onTransformEndRef.current = onTransformEnd;
70
- const onPressRef = useRef(onPress);
71
- onPressRef.current = onPress;
72
-
73
- // Start-of-gesture saved values
74
- const offsetRef = useRef({ x: initialX, y: initialY });
75
- const scaleStartRef = useRef(scale);
76
- const rotationStartRef = useRef(rotation); // degrees
77
-
78
- const emitTransform = useCallback(() => {
79
- onTransformEndRef.current({
80
- x: positionRef.current.x,
81
- y: positionRef.current.y,
82
- scale: scaleRef.current,
83
- rotation: rotationRef.current,
84
- });
85
- }, []);
86
-
87
- const panGesture = Gesture.Pan()
88
- .runOnJS(true)
89
- .averageTouches(true)
90
- .onStart(() => {
91
- offsetRef.current = { x: positionRef.current.x, y: positionRef.current.y };
92
- })
93
- .onUpdate((e) => {
94
- setPosition({
95
- x: offsetRef.current.x + e.translationX,
96
- y: offsetRef.current.y + e.translationY,
97
- });
98
- })
99
- .onEnd(emitTransform);
100
-
101
- const pinchGesture = Gesture.Pinch()
102
- .runOnJS(true)
103
- .onStart(() => {
104
- scaleStartRef.current = scaleRef.current;
105
- })
106
- .onUpdate((e) => {
107
- setScale(Math.max(0.2, Math.min(6, scaleStartRef.current * e.scale)));
108
- })
109
- .onEnd(emitTransform);
110
-
111
- const rotationGesture = Gesture.Rotation()
112
- .runOnJS(true)
113
- .onStart(() => {
114
- rotationStartRef.current = rotationRef.current;
115
- })
116
- .onUpdate((e) => {
117
- setRotation(rotationStartRef.current + (e.rotation * 180) / Math.PI);
118
- })
119
- .onEnd(emitTransform);
120
-
121
- const tapGesture = Gesture.Tap()
122
- .runOnJS(true)
123
- .onEnd(() => onPressRef.current());
124
-
125
- const composed = Gesture.Exclusive(
126
- Gesture.Simultaneous(panGesture, pinchGesture, rotationGesture),
127
- tapGesture,
128
- );
129
-
130
- return (
131
- <GestureDetector gesture={composed}>
132
- <View
133
- accessibilityLabel={text || "Text layer"}
134
- accessibilityRole="button"
135
- style={[
136
- styles.container,
137
- {
138
- transform: [
139
- { translateX: position.x },
140
- { translateY: position.y },
141
- { rotate: `${rotation}deg` },
142
- { scale },
143
- ],
144
- opacity,
145
- zIndex: isSelected ? 100 : 10,
146
- },
147
- ]}
148
- >
149
- <View
150
- style={{
151
- padding: tokens.spacing.xs,
152
- borderRadius: tokens.borders.radius.sm,
153
- borderWidth: isSelected ? 2 : 0,
154
- borderColor: tokens.colors.primary,
155
- borderStyle: "dashed",
156
- backgroundColor: isSelected
157
- ? tokens.colors.primary + "10"
158
- : backgroundColor,
159
- }}
160
- >
161
- <AtomicText
162
- style={{
163
- fontSize,
164
- fontFamily: fontFamily === "System" ? undefined : fontFamily,
165
- color,
166
- textAlign,
167
- fontWeight: isBold ? "900" : "normal",
168
- fontStyle: isItalic ? "italic" : "normal",
169
- }}
170
- >
171
- {text || "TAP TO EDIT"}
172
- </AtomicText>
173
- </View>
174
- </View>
175
- </GestureDetector>
176
- );
177
- };
178
-
179
- const styles = StyleSheet.create({
180
- container: { position: "absolute" },
181
- });
@@ -1,106 +0,0 @@
1
- import React from "react";
2
- import { View, StyleSheet } from "react-native";
3
- import { Image } from "expo-image";
4
- import { DraggableText, LayerTransform } from "./DraggableText";
5
- import { DraggableSticker } from "./DraggableSticker";
6
- import { Layer, ImageFilters } from "../types";
7
-
8
- interface EditorCanvasProps {
9
- imageUrl: string;
10
- layers: Layer[];
11
- activeLayerId: string | null;
12
- filters: ImageFilters;
13
- onLayerTap: (layerId: string) => void;
14
- onLayerTransform: (layerId: string, transform: LayerTransform) => void;
15
- styles: {
16
- canvas: object;
17
- canvasImage: object;
18
- };
19
- }
20
-
21
- export const EditorCanvas: React.FC<EditorCanvasProps> = ({
22
- imageUrl,
23
- layers,
24
- activeLayerId,
25
- filters,
26
- onLayerTap,
27
- onLayerTransform,
28
- styles: externalStyles,
29
- }) => {
30
- // Basic brightness preview: dark overlay for < 1, light for > 1
31
- const brightness = filters.brightness ?? 1;
32
- const brightnessOverlay =
33
- brightness < 1
34
- ? { color: "black", opacity: Math.min(0.6, 1 - brightness) }
35
- : brightness > 1
36
- ? { color: "white", opacity: Math.min(0.4, brightness - 1) }
37
- : null;
38
-
39
- return (
40
- <View style={externalStyles.canvas}>
41
- <Image
42
- source={{ uri: imageUrl }}
43
- style={externalStyles.canvasImage}
44
- contentFit="cover"
45
- />
46
-
47
- {/* Brightness visual overlay */}
48
- {brightnessOverlay && (
49
- <View
50
- style={[
51
- StyleSheet.absoluteFill,
52
- {
53
- backgroundColor: brightnessOverlay.color,
54
- opacity: brightnessOverlay.opacity,
55
- },
56
- ]}
57
- pointerEvents="none"
58
- />
59
- )}
60
-
61
- {layers.map((layer) => {
62
- if (layer.type === "text") {
63
- return (
64
- <DraggableText
65
- key={layer.id}
66
- text={layer.text || "Tap to edit"}
67
- color={layer.color}
68
- fontSize={layer.fontSize}
69
- fontFamily={layer.fontFamily}
70
- textAlign={layer.textAlign}
71
- rotation={layer.rotation}
72
- scale={layer.scale}
73
- opacity={layer.opacity}
74
- backgroundColor={layer.backgroundColor}
75
- isBold={layer.isBold}
76
- isItalic={layer.isItalic}
77
- initialX={layer.x}
78
- initialY={layer.y}
79
- onTransformEnd={(t) => onLayerTransform(layer.id, t)}
80
- onPress={() => onLayerTap(layer.id)}
81
- isSelected={activeLayerId === layer.id}
82
- />
83
- );
84
- } else if (layer.type === "sticker") {
85
- return (
86
- <DraggableSticker
87
- key={layer.id}
88
- uri={layer.uri}
89
- initialX={layer.x}
90
- initialY={layer.y}
91
- rotation={layer.rotation}
92
- scale={layer.scale}
93
- opacity={layer.opacity}
94
- onTransformEnd={(t) => onLayerTransform(layer.id, t)}
95
- onPress={() => onLayerTap(layer.id)}
96
- isSelected={activeLayerId === layer.id}
97
- />
98
- );
99
- }
100
- return null;
101
- })}
102
- </View>
103
- );
104
- };
105
-
106
- export default React.memo(EditorCanvas);
@@ -1,155 +0,0 @@
1
- import React from "react";
2
- import { View, TouchableOpacity } 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
-
6
- interface EditorToolbarProps {
7
- onAddText: () => void;
8
- onAddSticker?: () => void;
9
- onOpenFilters?: () => void;
10
- onOpenAdjustments?: () => void;
11
- onOpenLayers: () => void;
12
- onAIMagic?: () => void;
13
- onUndo?: () => void;
14
- onRedo?: () => void;
15
- canUndo?: boolean;
16
- canRedo?: boolean;
17
- t: (key: string) => string;
18
- styles: {
19
- bottomToolbar: object;
20
- toolButton: object;
21
- toolButtonActive: object;
22
- aiMagicButton: object;
23
- [key: string]: object;
24
- };
25
- }
26
-
27
- const ToolButton = ({
28
- icon,
29
- label,
30
- onPress,
31
- isActive,
32
- disabled,
33
- parentStyles,
34
- }: {
35
- icon: string;
36
- label: string;
37
- onPress: () => void;
38
- isActive?: boolean;
39
- disabled?: boolean;
40
- parentStyles: EditorToolbarProps["styles"];
41
- }) => (
42
- <TouchableOpacity
43
- style={[parentStyles.toolButton, isActive && parentStyles.toolButtonActive]}
44
- onPress={onPress}
45
- disabled={disabled}
46
- accessibilityLabel={label}
47
- accessibilityRole="button"
48
- accessibilityState={{ selected: isActive, disabled }}
49
- >
50
- <AtomicIcon
51
- name={icon as "edit"}
52
- size="md"
53
- color={disabled ? "textSecondary" : isActive ? "primary" : "textSecondary"}
54
- />
55
- <AtomicText
56
- type="labelSmall"
57
- color={disabled ? "textSecondary" : isActive ? "primary" : "textSecondary"}
58
- >
59
- {label}
60
- </AtomicText>
61
- </TouchableOpacity>
62
- );
63
-
64
- export const EditorToolbar: React.FC<EditorToolbarProps> = ({
65
- onAddText,
66
- onAddSticker,
67
- onOpenFilters,
68
- onOpenAdjustments,
69
- onOpenLayers,
70
- onAIMagic,
71
- onUndo,
72
- onRedo,
73
- canUndo = false,
74
- canRedo = false,
75
- styles: parentStyles,
76
- t,
77
- }) => {
78
- const tokens = useAppDesignTokens();
79
-
80
- return (
81
- <View style={parentStyles.bottomToolbar}>
82
- {onUndo && (
83
- <ToolButton
84
- icon="arrow-back"
85
- label={t("photo_editor.undo") || "Undo"}
86
- onPress={onUndo}
87
- disabled={!canUndo}
88
- parentStyles={parentStyles}
89
- />
90
- )}
91
-
92
- <ToolButton
93
- icon="edit"
94
- label={t("photo_editor.text") || "Text"}
95
- onPress={onAddText}
96
- parentStyles={parentStyles}
97
- />
98
-
99
- {onAddSticker && (
100
- <ToolButton
101
- icon="sparkles"
102
- label={t("photo_editor.sticker") || "Sticker"}
103
- onPress={onAddSticker}
104
- parentStyles={parentStyles}
105
- />
106
- )}
107
-
108
- {onAIMagic && (
109
- <TouchableOpacity
110
- style={parentStyles.aiMagicButton}
111
- onPress={onAIMagic}
112
- accessibilityLabel="AI Magic"
113
- accessibilityRole="button"
114
- >
115
- <AtomicIcon name="sparkles" size="lg" customColor={tokens.colors.onPrimary} />
116
- </TouchableOpacity>
117
- )}
118
-
119
- {onOpenAdjustments && (
120
- <ToolButton
121
- icon="flash"
122
- label={t("photo_editor.adjust") || "Adjust"}
123
- onPress={onOpenAdjustments}
124
- parentStyles={parentStyles}
125
- />
126
- )}
127
-
128
- {onOpenFilters && (
129
- <ToolButton
130
- icon="brush"
131
- label={t("photo_editor.filters") || "Filters"}
132
- onPress={onOpenFilters}
133
- parentStyles={parentStyles}
134
- />
135
- )}
136
-
137
- <ToolButton
138
- icon="copy"
139
- label={t("photo_editor.layers") || "Layers"}
140
- onPress={onOpenLayers}
141
- parentStyles={parentStyles}
142
- />
143
-
144
- {onRedo && (
145
- <ToolButton
146
- icon="chevron-forward"
147
- label={t("photo_editor.redo") || "Redo"}
148
- onPress={onRedo}
149
- disabled={!canRedo}
150
- parentStyles={parentStyles}
151
- />
152
- )}
153
- </View>
154
- );
155
- };
@@ -1,73 +0,0 @@
1
- import React, { useMemo } from "react";
2
- import { View, 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 { DEFAULT_FILTERS, type FilterOption } from "../constants";
6
-
7
- interface FilterPickerProps {
8
- selectedFilter: string;
9
- onSelectFilter: (option: FilterOption) => void;
10
- filters?: FilterOption[];
11
- }
12
-
13
- export const FilterPicker: React.FC<FilterPickerProps> = ({
14
- selectedFilter,
15
- onSelectFilter,
16
- filters = DEFAULT_FILTERS,
17
- }) => {
18
- const tokens = useAppDesignTokens();
19
-
20
- const styles = useMemo(() => StyleSheet.create({
21
- container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
22
- grid: { flexDirection: "row", flexWrap: "wrap", gap: tokens.spacing.sm },
23
- filter: {
24
- width: 75,
25
- height: 75,
26
- borderRadius: tokens.borders.radius.md,
27
- backgroundColor: tokens.colors.surfaceVariant,
28
- alignItems: "center",
29
- justifyContent: "center",
30
- borderWidth: 2,
31
- borderColor: "transparent",
32
- },
33
- active: {
34
- borderColor: tokens.colors.primary,
35
- backgroundColor: tokens.colors.primary + "10",
36
- },
37
- }), [tokens]);
38
-
39
- return (
40
- <View style={styles.container}>
41
- <AtomicText type="headlineSmall">Filters</AtomicText>
42
- <View style={styles.grid}>
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 }}
53
- >
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
- })}
68
- </View>
69
- </View>
70
- );
71
- };
72
-
73
- export default React.memo(FilterPicker);
@@ -1,132 +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
-
6
- interface FontControlsProps {
7
- fontSize: number;
8
- selectedFont: string;
9
- fonts: readonly string[];
10
- onFontSizeChange: (size: number) => void;
11
- onFontSelect: (font: string) => void;
12
- styles: {
13
- controlsPanel: object;
14
- fontRow: object;
15
- fontChip: object;
16
- fontChipActive: object;
17
- [key: string]: object;
18
- };
19
- }
20
-
21
- const MIN_FONT_SIZE = 12;
22
- const MAX_FONT_SIZE = 128;
23
- const FONT_SIZE_STEP = 4;
24
-
25
- export const FontControls: React.FC<FontControlsProps> = ({
26
- fontSize,
27
- selectedFont,
28
- fonts,
29
- onFontSizeChange,
30
- onFontSelect,
31
- styles: externalStyles,
32
- }) => {
33
- const tokens = useAppDesignTokens();
34
-
35
- const styles = useMemo(() => StyleSheet.create({
36
- stepRow: {
37
- flexDirection: "row",
38
- gap: tokens.spacing.sm,
39
- marginBottom: tokens.spacing.md,
40
- },
41
- stepBtn: {
42
- padding: tokens.spacing.sm,
43
- backgroundColor: tokens.colors.surface,
44
- borderRadius: tokens.borders.radius.sm,
45
- borderWidth: 1,
46
- borderColor: tokens.colors.border,
47
- minWidth: 44,
48
- alignItems: "center",
49
- },
50
- sizeRow: {
51
- flexDirection: "row",
52
- alignItems: "center",
53
- justifyContent: "space-between",
54
- marginBottom: tokens.spacing.sm,
55
- },
56
- sizeLabel: {
57
- flexDirection: "row",
58
- alignItems: "center",
59
- gap: tokens.spacing.xs,
60
- },
61
- }), [tokens]);
62
-
63
- return (
64
- <View style={externalStyles.controlsPanel}>
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>
71
- </View>
72
- <AtomicText fontWeight="bold" color="primary">
73
- {fontSize}px
74
- </AtomicText>
75
- </View>
76
-
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>
85
- </TouchableOpacity>
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
- >
92
- <AtomicText fontWeight="bold">+</AtomicText>
93
- </TouchableOpacity>
94
- </View>
95
-
96
- <AtomicText
97
- type="labelMedium"
98
- color="textSecondary"
99
- style={{ marginBottom: tokens.spacing.xs }}
100
- >
101
- Font Style
102
- </AtomicText>
103
- <ScrollView horizontal showsHorizontalScrollIndicator={false}>
104
- <View style={externalStyles.fontRow}>
105
- {fonts.map((font) => (
106
- <TouchableOpacity
107
- key={font}
108
- style={[
109
- externalStyles.fontChip,
110
- selectedFont === font && externalStyles.fontChipActive,
111
- ]}
112
- onPress={() => onFontSelect(font)}
113
- accessibilityLabel={`Font: ${font}`}
114
- accessibilityRole="button"
115
- accessibilityState={{ selected: selectedFont === font }}
116
- >
117
- <AtomicText
118
- fontWeight="bold"
119
- color={selectedFont === font ? "onPrimary" : "textSecondary"}
120
- style={{ fontFamily: font === "System" ? undefined : font }}
121
- >
122
- {font}
123
- </AtomicText>
124
- </TouchableOpacity>
125
- ))}
126
- </View>
127
- </ScrollView>
128
- </View>
129
- );
130
- };
131
-
132
- export default React.memo(FontControls);