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

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.
@@ -3,14 +3,14 @@
3
3
  * Full subtitle editor panel: list + add/edit modal
4
4
  */
5
5
 
6
- import React, { useState, useCallback, useMemo } from "react";
7
- import { View, ScrollView, TouchableOpacity, Modal, TextInput, StyleSheet } from "react-native";
6
+ import React, { useMemo } from "react";
7
+ import { View, ScrollView } 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 { SubtitleTimeInput } from "./SubtitleTimeInput";
11
- import { SubtitleStylePicker } from "./SubtitleStylePicker";
12
- import { DEFAULT_SUBTITLE_STYLE } from "../../infrastructure/constants/subtitle.constants";
13
- import { formatTimeDetailed } from "../../infrastructure/utils/srt.utils";
10
+ import { SubtitleListHeader } from "./subtitle/SubtitleListHeader";
11
+ import { SubtitleListItem } from "./subtitle/SubtitleListItem";
12
+ import { SubtitleModal } from "./subtitle/SubtitleModal";
13
+ import { useSubtitleForm } from "./subtitle/useSubtitleForm";
14
14
  import type { Subtitle, SubtitleStyle } from "../../domain/entities/video-project.types";
15
15
 
16
16
  interface SubtitleListPanelProps {
@@ -34,272 +34,66 @@ export const SubtitleListPanel: React.FC<SubtitleListPanelProps> = ({
34
34
  }) => {
35
35
  const tokens = useAppDesignTokens();
36
36
 
37
- const [showModal, setShowModal] = useState(false);
38
- const [editing, setEditing] = useState<Subtitle | null>(null);
39
- const [text, setText] = useState("");
40
- const [startTime, setStartTime] = useState(0);
41
- const [endTime, setEndTime] = useState(3);
42
- const [style, setStyle] = useState<SubtitleStyle>({ ...DEFAULT_SUBTITLE_STYLE });
43
-
44
- const styles = useMemo(() => StyleSheet.create({
45
- header: {
46
- flexDirection: "row",
47
- alignItems: "center",
48
- justifyContent: "space-between",
49
- paddingHorizontal: tokens.spacing.md,
50
- paddingVertical: tokens.spacing.sm,
51
- borderBottomWidth: 1,
52
- borderBottomColor: tokens.colors.border,
53
- },
54
- addBtn: {
55
- width: 34,
56
- height: 34,
57
- borderRadius: 17,
58
- backgroundColor: tokens.colors.primary,
59
- alignItems: "center",
60
- justifyContent: "center",
61
- },
62
- emptyBox: {
63
- alignItems: "center",
64
- paddingTop: tokens.spacing.xl,
65
- gap: tokens.spacing.sm,
66
- },
67
- item: {
68
- flexDirection: "row",
69
- alignItems: "center",
70
- backgroundColor: tokens.colors.surface,
71
- borderRadius: tokens.borders.radius.md,
72
- marginHorizontal: tokens.spacing.md,
73
- marginBottom: tokens.spacing.sm,
74
- borderWidth: 1,
75
- borderColor: tokens.colors.border,
76
- overflow: "hidden",
77
- },
78
- itemActive: {
79
- borderColor: tokens.colors.primary,
80
- backgroundColor: tokens.colors.primaryContainer,
81
- },
82
- itemBar: { width: 4, alignSelf: "stretch", backgroundColor: tokens.colors.surfaceVariant },
83
- itemBarActive: { backgroundColor: tokens.colors.primary },
84
- itemContent: { flex: 1, padding: tokens.spacing.sm },
85
- itemTimeRow: { flexDirection: "row", alignItems: "center", gap: tokens.spacing.xs, marginBottom: 2 },
86
- itemEditBtn: {
87
- width: 40,
88
- height: 40,
89
- alignItems: "center",
90
- justifyContent: "center",
91
- },
92
- // Modal
93
- overlay: {
94
- flex: 1,
95
- backgroundColor: "rgba(0,0,0,0.5)",
96
- justifyContent: "flex-end",
97
- },
98
- modal: {
99
- backgroundColor: tokens.colors.surface,
100
- borderTopLeftRadius: tokens.borders.radius.xl,
101
- borderTopRightRadius: tokens.borders.radius.xl,
102
- paddingHorizontal: tokens.spacing.md,
103
- paddingTop: tokens.spacing.md,
104
- paddingBottom: tokens.spacing.xl,
105
- maxHeight: "90%",
106
- },
107
- handle: {
108
- width: 36,
109
- height: 4,
110
- borderRadius: 2,
111
- backgroundColor: tokens.colors.border,
112
- alignSelf: "center",
113
- marginBottom: tokens.spacing.md,
114
- },
115
- textInput: {
116
- backgroundColor: tokens.colors.surfaceVariant,
117
- borderRadius: tokens.borders.radius.md,
118
- padding: tokens.spacing.md,
119
- color: tokens.colors.textPrimary,
120
- fontSize: 16,
121
- minHeight: 80,
122
- textAlignVertical: "top",
123
- marginBottom: tokens.spacing.md,
124
- borderWidth: 1,
125
- borderColor: tokens.colors.border,
126
- },
127
- timeRow: {
128
- flexDirection: "row",
129
- gap: tokens.spacing.md,
130
- marginBottom: tokens.spacing.md,
131
- },
132
- actionRow: {
133
- flexDirection: "row",
134
- gap: tokens.spacing.md,
135
- marginTop: tokens.spacing.md,
136
- },
137
- cancelBtn: {
138
- flex: 1,
139
- paddingVertical: tokens.spacing.md,
140
- borderRadius: tokens.borders.radius.md,
141
- backgroundColor: tokens.colors.surfaceVariant,
142
- alignItems: "center",
143
- },
144
- saveBtn: {
145
- flex: 1,
146
- paddingVertical: tokens.spacing.md,
147
- borderRadius: tokens.borders.radius.md,
148
- backgroundColor: tokens.colors.primary,
149
- alignItems: "center",
150
- },
151
- saveBtnDisabled: { opacity: 0.4 },
152
- }), [tokens]);
37
+ const form = useSubtitleForm();
153
38
 
154
39
  const activeId = useMemo(() => {
155
40
  return subtitles.find((s) => currentTime >= s.startTime && currentTime <= s.endTime)?.id ?? null;
156
41
  }, [subtitles, currentTime]);
157
42
 
158
- const openAdd = useCallback(() => {
159
- setEditing(null);
160
- setText("");
161
- setStartTime(Math.floor(currentTime));
162
- setEndTime(Math.floor(currentTime) + 3);
163
- setStyle({ ...DEFAULT_SUBTITLE_STYLE });
164
- setShowModal(true);
165
- }, [currentTime]);
166
-
167
- const openEdit = useCallback((sub: Subtitle) => {
168
- setEditing(sub);
169
- setText(sub.text);
170
- setStartTime(sub.startTime);
171
- setEndTime(sub.endTime);
172
- setStyle({ ...sub.style });
173
- setShowModal(true);
174
- }, []);
175
-
176
- const handleSave = useCallback(() => {
177
- if (!text.trim()) return;
178
- const resolvedEnd = Math.max(endTime, startTime + 0.5);
179
- if (editing) {
180
- onUpdate(editing.id, { text: text.trim(), startTime, endTime: resolvedEnd, style });
181
- } else {
182
- onAdd(text.trim(), startTime, resolvedEnd, style);
183
- }
184
- setShowModal(false);
185
- }, [text, editing, startTime, endTime, style, onAdd, onUpdate]);
43
+ const emptyStyles = {
44
+ emptyBox: {
45
+ alignItems: "center" as const,
46
+ paddingTop: tokens.spacing.xl,
47
+ gap: tokens.spacing.sm,
48
+ },
49
+ };
186
50
 
187
51
  return (
188
52
  <View style={{ flex: 1 }}>
189
- {/* Header */}
190
- <View style={styles.header}>
191
- <AtomicText fontWeight="semibold" color="textPrimary">
192
- {t("subtitle.panel.title") || "Subtitles"} ({subtitles.length})
193
- </AtomicText>
194
- <TouchableOpacity style={styles.addBtn} onPress={openAdd} accessibilityRole="button">
195
- <AtomicIcon name="add" size="sm" color="onPrimary" />
196
- </TouchableOpacity>
197
- </View>
53
+ <SubtitleListHeader
54
+ count={subtitles.length}
55
+ onAdd={() => form.openAdd(currentTime)}
56
+ title={t("subtitle.panel.title") || "Subtitles"}
57
+ />
198
58
 
199
- {/* List */}
200
59
  <ScrollView contentContainerStyle={{ paddingVertical: tokens.spacing.sm }}>
201
60
  {subtitles.length === 0 ? (
202
- <View style={styles.emptyBox}>
61
+ <View style={emptyStyles.emptyBox}>
203
62
  <AtomicIcon name="video" size="lg" color="textSecondary" />
204
- <AtomicText color="textSecondary">{t("subtitle.panel.empty") || "No subtitles yet"}</AtomicText>
63
+ <AtomicText color="textSecondary">
64
+ {t("subtitle.panel.empty") || "No subtitles yet"}
65
+ </AtomicText>
205
66
  <AtomicText type="labelSmall" color="textTertiary">
206
67
  {t("subtitle.panel.emptyHint") || "Tap + to add a subtitle"}
207
68
  </AtomicText>
208
69
  </View>
209
70
  ) : (
210
- subtitles.map((sub) => {
211
- const isActive = sub.id === activeId;
212
- return (
213
- <TouchableOpacity
214
- key={sub.id}
215
- style={[styles.item, isActive && styles.itemActive]}
216
- onPress={() => onSeek(sub.startTime)}
217
- accessibilityRole="button"
218
- >
219
- <View style={[styles.itemBar, isActive && styles.itemBarActive]} />
220
- <View style={styles.itemContent}>
221
- <View style={styles.itemTimeRow}>
222
- <AtomicText type="labelSmall" color="textSecondary">
223
- {formatTimeDetailed(sub.startTime)}
224
- </AtomicText>
225
- <AtomicText type="labelSmall" color="textTertiary">→</AtomicText>
226
- <AtomicText type="labelSmall" color="textSecondary">
227
- {formatTimeDetailed(sub.endTime)}
228
- </AtomicText>
229
- </View>
230
- <AtomicText color="textPrimary" numberOfLines={2}>
231
- {sub.text}
232
- </AtomicText>
233
- </View>
234
- <TouchableOpacity
235
- style={styles.itemEditBtn}
236
- onPress={() => openEdit(sub)}
237
- accessibilityRole="button"
238
- >
239
- <AtomicIcon name="edit" size="sm" color="textSecondary" />
240
- </TouchableOpacity>
241
- </TouchableOpacity>
242
- );
243
- })
71
+ subtitles.map((subtitle) => (
72
+ <SubtitleListItem
73
+ key={subtitle.id}
74
+ subtitle={subtitle}
75
+ isActive={subtitle.id === activeId}
76
+ onEdit={form.openEdit}
77
+ onSeek={onSeek}
78
+ />
79
+ ))
244
80
  )}
245
81
  </ScrollView>
246
82
 
247
- {/* Add/Edit Modal */}
248
- <Modal visible={showModal} animationType="slide" transparent onRequestClose={() => setShowModal(false)}>
249
- <TouchableOpacity style={styles.overlay} activeOpacity={1} onPress={() => setShowModal(false)}>
250
- <TouchableOpacity style={styles.modal} activeOpacity={1} onPress={() => {}}>
251
- <View style={styles.handle} />
252
- <ScrollView showsVerticalScrollIndicator={false}>
253
- <AtomicText fontWeight="bold" color="textPrimary" style={{ marginBottom: tokens.spacing.md, fontSize: 18 }}>
254
- {editing ? t("subtitle.modal.edit") || "Edit Subtitle" : t("subtitle.modal.add") || "Add Subtitle"}
255
- </AtomicText>
256
-
257
- <TextInput
258
- style={styles.textInput}
259
- value={text}
260
- onChangeText={setText}
261
- placeholder={t("subtitle.modal.placeholder") || "Enter subtitle text…"}
262
- placeholderTextColor={tokens.colors.textSecondary}
263
- multiline
264
- numberOfLines={3}
265
- />
266
-
267
- <View style={styles.timeRow}>
268
- <SubtitleTimeInput
269
- label={t("subtitle.modal.startTime") || "Start"}
270
- value={startTime}
271
- onChange={setStartTime}
272
- />
273
- <SubtitleTimeInput
274
- label={t("subtitle.modal.endTime") || "End"}
275
- value={endTime}
276
- onChange={setEndTime}
277
- />
278
- </View>
279
-
280
- <SubtitleStylePicker style={style} previewText={text} onChange={setStyle} t={t} />
281
-
282
- <View style={styles.actionRow}>
283
- <TouchableOpacity style={styles.cancelBtn} onPress={() => setShowModal(false)} accessibilityRole="button">
284
- <AtomicText fontWeight="semibold" color="textSecondary">
285
- {t("common.cancel") || "Cancel"}
286
- </AtomicText>
287
- </TouchableOpacity>
288
- <TouchableOpacity
289
- style={[styles.saveBtn, !text.trim() && styles.saveBtnDisabled]}
290
- onPress={handleSave}
291
- disabled={!text.trim()}
292
- accessibilityRole="button"
293
- >
294
- <AtomicText fontWeight="semibold" color="onPrimary">
295
- {t("common.save") || "Save"}
296
- </AtomicText>
297
- </TouchableOpacity>
298
- </View>
299
- </ScrollView>
300
- </TouchableOpacity>
301
- </TouchableOpacity>
302
- </Modal>
83
+ <SubtitleModal
84
+ visible={form.showModal}
85
+ editing={!!form.editing}
86
+ text={form.text}
87
+ startTime={form.startTime}
88
+ endTime={form.endTime}
89
+ style={form.style}
90
+ onChangeText={form.setText}
91
+ onChangeStartTime={form.setStartTime}
92
+ onChangeEndTime={form.setEndTime}
93
+ onChangeStyle={form.setStyle}
94
+ onSave={() => form.save(onAdd, onUpdate)}
95
+ onCancel={form.close}
96
+ />
303
97
  </View>
304
98
  );
305
99
  };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * CollageCanvas Component
3
+ * Main canvas rendering for collage editor
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity } from "react-native";
8
+ import { Image } from "expo-image";
9
+ import { AtomicIcon } from "@umituz/react-native-design-system/atoms";
10
+ import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
+ import type { CollageLayout } from "../../../infrastructure/constants/collage.constants";
12
+
13
+ interface CollageCanvasProps {
14
+ layout: CollageLayout;
15
+ images: (string | null)[];
16
+ spacing: number;
17
+ borderRadius: number;
18
+ onCellPress: (index: number) => void;
19
+ size: number;
20
+ }
21
+
22
+ export const CollageCanvas: React.FC<CollageCanvasProps> = ({
23
+ layout,
24
+ images,
25
+ spacing,
26
+ borderRadius,
27
+ onCellPress,
28
+ size,
29
+ }) => {
30
+ const tokens = useAppDesignTokens();
31
+
32
+ const styles = {
33
+ canvas: {
34
+ width: size,
35
+ height: size,
36
+ alignSelf: "center" as const,
37
+ position: "relative" as const,
38
+ backgroundColor: tokens.colors.surface,
39
+ borderRadius: tokens.borders.radius.md,
40
+ overflow: "hidden" as const,
41
+ },
42
+ cell: {
43
+ position: "absolute" as const,
44
+ overflow: "hidden" as const,
45
+ },
46
+ cellImage: {
47
+ width: "100%" as const,
48
+ height: "100%" as const,
49
+ },
50
+ cellEmpty: {
51
+ flex: 1,
52
+ backgroundColor: tokens.colors.surfaceVariant,
53
+ alignItems: "center" as const,
54
+ justifyContent: "center" as const,
55
+ },
56
+ };
57
+
58
+ return (
59
+ <View style={styles.canvas}>
60
+ {layout.grid.map((cell, index) => {
61
+ const [cx, cy, cw, ch] = cell;
62
+ const cellStyle = {
63
+ left: cx * size + spacing,
64
+ top: cy * size + spacing,
65
+ width: cw * size - spacing * 2,
66
+ height: ch * size - spacing * 2,
67
+ borderRadius,
68
+ };
69
+
70
+ return (
71
+ <TouchableOpacity
72
+ key={index}
73
+ style={[styles.cell, cellStyle]}
74
+ onPress={() => onCellPress(index)}
75
+ accessibilityLabel={`Cell ${index + 1}`}
76
+ accessibilityRole="button"
77
+ >
78
+ {images[index] ? (
79
+ <Image
80
+ source={{ uri: images[index]! }}
81
+ style={[styles.cellImage, { borderRadius }]}
82
+ contentFit="cover"
83
+ />
84
+ ) : (
85
+ <View style={styles.cellEmpty}>
86
+ <AtomicIcon name="add" size="md" color="textSecondary" />
87
+ </View>
88
+ )}
89
+ </TouchableOpacity>
90
+ );
91
+ })}
92
+ </View>
93
+ );
94
+ };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * CollageControls Component
3
+ * Controls for spacing and border radius in collage editor
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 CollageControlsProps {
12
+ spacing: number;
13
+ borderRadius: number;
14
+ onSpacingChange: (value: number) => void;
15
+ onBorderRadiusChange: (value: number) => void;
16
+ t: (key: string) => string;
17
+ }
18
+
19
+ export const CollageControls: React.FC<CollageControlsProps> = ({
20
+ spacing,
21
+ borderRadius,
22
+ onSpacingChange,
23
+ onBorderRadiusChange,
24
+ t,
25
+ }) => {
26
+ const tokens = useAppDesignTokens();
27
+
28
+ const styles = {
29
+ controls: {
30
+ paddingHorizontal: tokens.spacing.md,
31
+ paddingTop: tokens.spacing.md,
32
+ gap: tokens.spacing.sm,
33
+ },
34
+ controlRow: {
35
+ flexDirection: "row" as const,
36
+ alignItems: "center" as const,
37
+ justifyContent: "space-between" as const,
38
+ },
39
+ stepper: {
40
+ flexDirection: "row" as const,
41
+ alignItems: "center" as const,
42
+ gap: tokens.spacing.sm,
43
+ },
44
+ stepBtn: {
45
+ width: 32,
46
+ height: 32,
47
+ borderRadius: 16,
48
+ backgroundColor: tokens.colors.surfaceVariant,
49
+ alignItems: "center" as const,
50
+ justifyContent: "center" as const,
51
+ },
52
+ stepValue: {
53
+ minWidth: 28,
54
+ textAlign: "center" as const,
55
+ },
56
+ };
57
+
58
+ return (
59
+ <View style={styles.controls}>
60
+ <View style={styles.controlRow}>
61
+ <AtomicText type="labelSmall" color="textSecondary">
62
+ {t("editor.collage.spacing") || "Spacing"}
63
+ </AtomicText>
64
+ <View style={styles.stepper}>
65
+ <TouchableOpacity
66
+ style={styles.stepBtn}
67
+ onPress={() => onSpacingChange(Math.max(0, spacing - 2))}
68
+ accessibilityLabel="Decrease spacing"
69
+ accessibilityRole="button"
70
+ >
71
+ <AtomicIcon name="chevron-back" size="sm" color="textSecondary" />
72
+ </TouchableOpacity>
73
+ <AtomicText fontWeight="bold" style={styles.stepValue}>
74
+ {spacing}
75
+ </AtomicText>
76
+ <TouchableOpacity
77
+ style={styles.stepBtn}
78
+ onPress={() => onSpacingChange(Math.min(16, spacing + 2))}
79
+ accessibilityLabel="Increase spacing"
80
+ accessibilityRole="button"
81
+ >
82
+ <AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
83
+ </TouchableOpacity>
84
+ </View>
85
+ </View>
86
+
87
+ <View style={styles.controlRow}>
88
+ <AtomicText type="labelSmall" color="textSecondary">
89
+ {t("editor.collage.borderRadius") || "Corner Radius"}
90
+ </AtomicText>
91
+ <View style={styles.stepper}>
92
+ <TouchableOpacity
93
+ style={styles.stepBtn}
94
+ onPress={() => onBorderRadiusChange(Math.max(0, borderRadius - 4))}
95
+ accessibilityLabel="Decrease border radius"
96
+ accessibilityRole="button"
97
+ >
98
+ <AtomicIcon name="chevron-back" size="sm" color="textSecondary" />
99
+ </TouchableOpacity>
100
+ <AtomicText fontWeight="bold" style={styles.stepValue}>
101
+ {borderRadius}
102
+ </AtomicText>
103
+ <TouchableOpacity
104
+ style={styles.stepBtn}
105
+ onPress={() => onBorderRadiusChange(Math.min(32, borderRadius + 4))}
106
+ accessibilityLabel="Increase border radius"
107
+ accessibilityRole="button"
108
+ >
109
+ <AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
110
+ </TouchableOpacity>
111
+ </View>
112
+ </View>
113
+ </View>
114
+ );
115
+ };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * CollageLayoutSelector Component
3
+ * Layout picker for collage editor
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, ScrollView, TouchableOpacity } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
9
+ import { COLLAGE_LAYOUTS } from "../../../infrastructure/constants/collage.constants";
10
+ import type { CollageLayout } from "../../../infrastructure/constants/collage.constants";
11
+
12
+ interface CollageLayoutSelectorProps {
13
+ selected: CollageLayout;
14
+ onSelect: (layout: CollageLayout) => void;
15
+ }
16
+
17
+ export const CollageLayoutSelector: React.FC<CollageLayoutSelectorProps> = ({
18
+ selected,
19
+ onSelect,
20
+ }) => {
21
+ const tokens = useAppDesignTokens();
22
+
23
+ const styles = {
24
+ layoutSection: {
25
+ paddingTop: tokens.spacing.sm,
26
+ },
27
+ layoutScroll: {
28
+ paddingHorizontal: tokens.spacing.md,
29
+ gap: tokens.spacing.sm,
30
+ },
31
+ layoutCard: {
32
+ width: 64,
33
+ alignItems: "center" as const,
34
+ gap: tokens.spacing.xs,
35
+ },
36
+ layoutPreview: {
37
+ width: 52,
38
+ height: 52,
39
+ borderRadius: tokens.borders.radius.sm,
40
+ overflow: "hidden" as const,
41
+ position: "relative" as const,
42
+ backgroundColor: tokens.colors.surfaceVariant,
43
+ borderWidth: 2,
44
+ borderColor: "transparent",
45
+ },
46
+ layoutPreviewActive: {
47
+ borderColor: tokens.colors.primary,
48
+ },
49
+ };
50
+
51
+ return (
52
+ <View style={styles.layoutSection}>
53
+ <ScrollView
54
+ horizontal
55
+ showsHorizontalScrollIndicator={false}
56
+ contentContainerStyle={styles.layoutScroll}
57
+ >
58
+ {COLLAGE_LAYOUTS.map((layout) => (
59
+ <TouchableOpacity
60
+ key={layout.id}
61
+ style={styles.layoutCard}
62
+ onPress={() => onSelect(layout)}
63
+ accessibilityLabel={`Layout ${layout.id}`}
64
+ accessibilityRole="button"
65
+ accessibilityState={{ selected: selected.id === layout.id }}
66
+ >
67
+ <View
68
+ style={[
69
+ styles.layoutPreview,
70
+ selected.id === layout.id && styles.layoutPreviewActive,
71
+ ]}
72
+ >
73
+ {layout.grid.map((cell, index) => {
74
+ const [cx, cy, cw, ch] = cell;
75
+ return (
76
+ <View
77
+ key={index}
78
+ style={{
79
+ position: "absolute",
80
+ left: `${cx * 100}%`,
81
+ top: `${cy * 100}%`,
82
+ width: `${cw * 100}%`,
83
+ height: `${ch * 100}%`,
84
+ borderWidth: 1,
85
+ borderColor: tokens.colors.border,
86
+ backgroundColor:
87
+ selected.id === layout.id
88
+ ? tokens.colors.primaryContainer
89
+ : "transparent",
90
+ }}
91
+ />
92
+ );
93
+ })}
94
+ </View>
95
+ </TouchableOpacity>
96
+ ))}
97
+ </ScrollView>
98
+ </View>
99
+ );
100
+ };