@umituz/react-native-ai-generation-content 1.17.16 → 1.17.18

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 (28) hide show
  1. package/package.json +1 -4
  2. package/src/domains/creations/presentation/components/CreationDetail/DetailImage.tsx +14 -14
  3. package/src/domains/creations/presentation/components/CreationDetail/DetailVideo.tsx +51 -58
  4. package/src/domains/creations/presentation/screens/CreationDetailScreen.tsx +36 -29
  5. package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +98 -69
  6. package/src/features/text-to-image/index.ts +92 -4
  7. package/src/features/text-to-image/presentation/components/AspectRatioSelector.tsx +98 -0
  8. package/src/features/text-to-image/presentation/components/ExamplePrompts.tsx +88 -0
  9. package/src/features/text-to-image/presentation/components/ImageSizeSelector.tsx +98 -0
  10. package/src/features/text-to-image/presentation/components/NumImagesSelector.tsx +93 -0
  11. package/src/features/text-to-image/presentation/components/OutputFormatSelector.tsx +98 -0
  12. package/src/features/text-to-image/presentation/components/SettingsSheet.tsx +139 -0
  13. package/src/features/text-to-image/presentation/components/StyleSelector.tsx +110 -0
  14. package/src/features/text-to-image/presentation/components/TextToImageGenerateButton.tsx +84 -0
  15. package/src/features/text-to-image/presentation/components/TextToImagePromptInput.tsx +90 -0
  16. package/src/features/text-to-image/presentation/components/index.ts +44 -0
  17. package/src/features/text-to-image/presentation/hooks/index.ts +25 -0
  18. package/src/features/text-to-image/presentation/hooks/useFormState.ts +103 -0
  19. package/src/features/text-to-image/presentation/hooks/useGeneration.ts +98 -0
  20. package/src/features/text-to-image/presentation/hooks/useTextToImageForm.ts +58 -0
  21. package/src/features/text-to-image/presentation/index.ts +6 -0
  22. package/src/features/text-to-voice/domain/types/component.types.ts +91 -0
  23. package/src/features/text-to-voice/domain/types/config.types.ts +34 -0
  24. package/src/features/text-to-voice/domain/types/form.types.ts +39 -0
  25. package/src/features/text-to-voice/domain/types/generation.types.ts +43 -0
  26. package/src/features/text-to-voice/domain/types/index.ts +31 -3
  27. package/src/features/text-to-voice/infrastructure/services/text-to-voice-executor.ts +2 -10
  28. package/src/features/text-to-voice/domain/types/text-to-voice.types.ts +0 -65
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.17.16",
3
+ "version": "1.17.18",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -101,8 +101,5 @@
101
101
  },
102
102
  "publishConfig": {
103
103
  "access": "public"
104
- },
105
- "dependencies": {
106
- "@umituz/react-native-ai-generation-content": "^1.17.14"
107
104
  }
108
105
  }
@@ -1,38 +1,38 @@
1
-
2
1
  import React from 'react';
3
- import { View, StyleSheet, Image, Dimensions } from 'react-native';
4
- import { useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
2
+ import { View, StyleSheet, useWindowDimensions } from 'react-native';
3
+ import { useAppDesignTokens } from "@umituz/react-native-design-system";
4
+ import { Image } from 'expo-image';
5
5
 
6
6
  interface DetailImageProps {
7
7
  readonly uri: string;
8
8
  }
9
9
 
10
- const { width } = Dimensions.get('window');
10
+ const HORIZONTAL_PADDING = 16;
11
+ const ASPECT_RATIO = 16 / 9;
11
12
 
12
13
  export const DetailImage: React.FC<DetailImageProps> = ({ uri }) => {
13
14
  const tokens = useAppDesignTokens();
14
- const styles = useStyles(tokens);
15
+ const { width } = useWindowDimensions();
16
+ const imageWidth = width - (HORIZONTAL_PADDING * 2);
17
+ const imageHeight = imageWidth / ASPECT_RATIO;
15
18
 
16
19
  return (
17
20
  <View style={styles.container}>
18
- <View style={styles.frame}>
19
- <Image source={{ uri }} style={styles.image} resizeMode="cover" />
21
+ <View style={[styles.frame, { width: imageWidth, height: imageHeight, backgroundColor: tokens.colors.surface }]}>
22
+ <Image source={{ uri }} style={styles.image} contentFit="cover" />
20
23
  </View>
21
24
  </View>
22
25
  );
23
26
  };
24
27
 
25
- const useStyles = (tokens: DesignTokens) => StyleSheet.create({
28
+ const styles = StyleSheet.create({
26
29
  container: {
27
- paddingHorizontal: tokens.spacing.lg,
28
- marginVertical: tokens.spacing.lg,
30
+ paddingHorizontal: HORIZONTAL_PADDING,
31
+ marginVertical: 16,
29
32
  },
30
33
  frame: {
31
- width: width - (tokens.spacing.lg * 2),
32
- height: width - (tokens.spacing.lg * 2),
33
- borderRadius: 24,
34
+ borderRadius: 16,
34
35
  overflow: 'hidden',
35
- backgroundColor: tokens.colors.surface,
36
36
  },
37
37
  image: {
38
38
  width: '100%',
@@ -4,12 +4,8 @@
4
4
  */
5
5
 
6
6
  import React, { useState, useCallback } from "react";
7
- import { View, StyleSheet, Dimensions, TouchableOpacity } from "react-native";
8
- import {
9
- useAppDesignTokens,
10
- AtomicIcon,
11
- type DesignTokens,
12
- } from "@umituz/react-native-design-system";
7
+ import { View, StyleSheet, TouchableOpacity, useWindowDimensions } from "react-native";
8
+ import { useAppDesignTokens, AtomicIcon } from "@umituz/react-native-design-system";
13
9
  import { Image } from "expo-image";
14
10
  import { useVideoPlayer, VideoView } from "expo-video";
15
11
 
@@ -18,14 +14,17 @@ interface DetailVideoProps {
18
14
  readonly thumbnailUrl?: string;
19
15
  }
20
16
 
21
- const { width } = Dimensions.get("window");
17
+ const HORIZONTAL_PADDING = 16;
18
+ const ASPECT_RATIO = 16 / 9;
22
19
 
23
20
  export const DetailVideo: React.FC<DetailVideoProps> = ({
24
21
  videoUrl,
25
22
  thumbnailUrl,
26
23
  }) => {
27
24
  const tokens = useAppDesignTokens();
28
- const styles = useStyles(tokens);
25
+ const { width } = useWindowDimensions();
26
+ const videoWidth = width - (HORIZONTAL_PADDING * 2);
27
+ const videoHeight = videoWidth / ASPECT_RATIO;
29
28
  const [isPlaying, setIsPlaying] = useState(false);
30
29
 
31
30
  const player = useVideoPlayer(videoUrl, (p) => {
@@ -39,7 +38,7 @@ export const DetailVideo: React.FC<DetailVideoProps> = ({
39
38
 
40
39
  return (
41
40
  <View style={styles.container}>
42
- <View style={styles.frame}>
41
+ <View style={[styles.frame, { width: videoWidth, height: videoHeight, backgroundColor: tokens.colors.surface }]}>
43
42
  {isPlaying ? (
44
43
  <VideoView
45
44
  player={player}
@@ -60,10 +59,10 @@ export const DetailVideo: React.FC<DetailVideoProps> = ({
60
59
  contentFit="cover"
61
60
  />
62
61
  ) : (
63
- <View style={styles.placeholder} />
62
+ <View style={[styles.placeholder, { backgroundColor: tokens.colors.surfaceSecondary }]} />
64
63
  )}
65
64
  <View style={styles.playButtonContainer}>
66
- <View style={styles.playButton}>
65
+ <View style={[styles.playButton, { backgroundColor: tokens.colors.primary }]}>
67
66
  <AtomicIcon name="play" customSize={32} color="onPrimary" />
68
67
  </View>
69
68
  </View>
@@ -74,50 +73,44 @@ export const DetailVideo: React.FC<DetailVideoProps> = ({
74
73
  );
75
74
  };
76
75
 
77
- const useStyles = (tokens: DesignTokens) =>
78
- StyleSheet.create({
79
- container: {
80
- paddingHorizontal: tokens.spacing.lg,
81
- marginVertical: tokens.spacing.lg,
82
- },
83
- frame: {
84
- width: width - tokens.spacing.lg * 2,
85
- height: width - tokens.spacing.lg * 2,
86
- borderRadius: 24,
87
- overflow: "hidden",
88
- backgroundColor: tokens.colors.surface,
89
- },
90
- video: {
91
- width: "100%",
92
- height: "100%",
93
- },
94
- thumbnailContainer: {
95
- width: "100%",
96
- height: "100%",
97
- justifyContent: "center",
98
- alignItems: "center",
99
- },
100
- thumbnail: {
101
- width: "100%",
102
- height: "100%",
103
- },
104
- placeholder: {
105
- width: "100%",
106
- height: "100%",
107
- backgroundColor: tokens.colors.surfaceSecondary,
108
- },
109
- playButtonContainer: {
110
- ...StyleSheet.absoluteFillObject,
111
- justifyContent: "center",
112
- alignItems: "center",
113
- },
114
- playButton: {
115
- width: 64,
116
- height: 64,
117
- borderRadius: 32,
118
- backgroundColor: tokens.colors.primary,
119
- justifyContent: "center",
120
- alignItems: "center",
121
- paddingLeft: 4,
122
- },
123
- });
76
+ const styles = StyleSheet.create({
77
+ container: {
78
+ paddingHorizontal: HORIZONTAL_PADDING,
79
+ marginVertical: 16,
80
+ },
81
+ frame: {
82
+ borderRadius: 16,
83
+ overflow: "hidden",
84
+ },
85
+ video: {
86
+ width: "100%",
87
+ height: "100%",
88
+ },
89
+ thumbnailContainer: {
90
+ width: "100%",
91
+ height: "100%",
92
+ justifyContent: "center",
93
+ alignItems: "center",
94
+ },
95
+ thumbnail: {
96
+ width: "100%",
97
+ height: "100%",
98
+ },
99
+ placeholder: {
100
+ width: "100%",
101
+ height: "100%",
102
+ },
103
+ playButtonContainer: {
104
+ ...StyleSheet.absoluteFillObject,
105
+ justifyContent: "center",
106
+ alignItems: "center",
107
+ },
108
+ playButton: {
109
+ width: 64,
110
+ height: 64,
111
+ borderRadius: 32,
112
+ justifyContent: "center",
113
+ alignItems: "center",
114
+ paddingLeft: 4,
115
+ },
116
+ });
@@ -1,6 +1,7 @@
1
1
  import React, { useMemo } from 'react';
2
- import { StyleSheet } from 'react-native';
3
- import { useAppDesignTokens, type DesignTokens, ScreenLayout } from "@umituz/react-native-design-system";
2
+ import { View, ScrollView, StyleSheet } from 'react-native';
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
+ import { useAppDesignTokens } from "@umituz/react-native-design-system";
4
5
  import type { Creation } from '../../domain/entities/Creation';
5
6
  import { hasVideoContent, getPreviewUrl } from '../../domain/utils';
6
7
  import { DetailHeader } from '../components/CreationDetail/DetailHeader';
@@ -37,6 +38,7 @@ export const CreationDetailScreen: React.FC<CreationDetailScreenProps> = ({
37
38
  t
38
39
  }) => {
39
40
  const tokens = useAppDesignTokens();
41
+ const insets = useSafeAreaInsets();
40
42
  const { getLocalizedTitle } = useCreationsProvider();
41
43
 
42
44
  // Extract data safely
@@ -57,48 +59,53 @@ export const CreationDetailScreen: React.FC<CreationDetailScreenProps> = ({
57
59
  return false;
58
60
  }, [creation.type, creation.output]);
59
61
 
60
- const styles = useStyles(tokens);
61
-
62
62
  // Get video URL and thumbnail for video content
63
63
  const videoUrl = creation.output?.videoUrl || creation.uri;
64
64
  const thumbnailUrl = getPreviewUrl(creation.output) || undefined;
65
65
 
66
66
  return (
67
- <ScreenLayout
68
- scrollable={true}
69
- edges={['top', 'bottom']}
70
- backgroundColor={tokens.colors.background}
71
- header={
67
+ <View style={[styles.container, { backgroundColor: tokens.colors.background }]}>
68
+ <View style={{ paddingTop: insets.top }}>
72
69
  <DetailHeader
73
70
  title={title}
74
71
  date={date}
75
72
  onClose={onClose}
76
73
  />
77
- }
78
- contentContainerStyle={styles.scrollContent}
79
- >
80
- {isVideo ? (
81
- <DetailVideo videoUrl={videoUrl} thumbnailUrl={thumbnailUrl} />
82
- ) : (
83
- <DetailImage uri={creation.uri} />
84
- )}
74
+ </View>
75
+ <ScrollView
76
+ style={styles.scrollView}
77
+ contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 32 }]}
78
+ showsVerticalScrollIndicator={false}
79
+ >
80
+ {isVideo ? (
81
+ <DetailVideo videoUrl={videoUrl} thumbnailUrl={thumbnailUrl} />
82
+ ) : (
83
+ <DetailImage uri={creation.uri} />
84
+ )}
85
85
 
86
- {story ? (
87
- <DetailStory story={story} />
88
- ) : null}
86
+ {story ? (
87
+ <DetailStory story={story} />
88
+ ) : null}
89
89
 
90
- <DetailActions
91
- onShare={() => onShare(creation)}
92
- onDelete={() => onDelete(creation)}
93
- shareLabel={t("result.shareButton")}
94
- deleteLabel={t("common.delete")}
95
- />
96
- </ScreenLayout>
90
+ <DetailActions
91
+ onShare={() => onShare(creation)}
92
+ onDelete={() => onDelete(creation)}
93
+ shareLabel={t("result.shareButton")}
94
+ deleteLabel={t("common.delete")}
95
+ />
96
+ </ScrollView>
97
+ </View>
97
98
  );
98
99
  };
99
100
 
100
- const useStyles = (tokens: DesignTokens) => StyleSheet.create({
101
+ const styles = StyleSheet.create({
102
+ container: {
103
+ flex: 1,
104
+ },
105
+ scrollView: {
106
+ flex: 1,
107
+ },
101
108
  scrollContent: {
102
- paddingBottom: tokens.spacing.xxl,
109
+ paddingTop: 8,
103
110
  },
104
111
  });
@@ -1,6 +1,8 @@
1
1
  declare const __DEV__: boolean;
2
2
 
3
3
  import React, { useMemo, useCallback, useState } from "react";
4
+ import { View, FlatList, RefreshControl, StyleSheet } from "react-native";
5
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
4
6
  import {
5
7
  useAppDesignTokens,
6
8
  useAlert,
@@ -9,7 +11,7 @@ import {
9
11
  useSharing,
10
12
  FilterBottomSheet,
11
13
  type BottomSheetModalRef,
12
- ScreenLayout,
14
+ type DesignTokens,
13
15
  } from "@umituz/react-native-design-system";
14
16
  import { useFocusEffect } from "@react-navigation/native";
15
17
  import { useCreations } from "../hooks/useCreations";
@@ -17,7 +19,7 @@ import { useDeleteCreation } from "../hooks/useDeleteCreation";
17
19
  import { useCreationsFilter } from "../hooks/useCreationsFilter";
18
20
  import {
19
21
  GalleryHeader,
20
- CreationsGrid,
22
+ CreationCard,
21
23
  CreationImageViewer,
22
24
  GalleryEmptyStates,
23
25
  } from "../components";
@@ -26,6 +28,7 @@ import type { Creation } from "../../domain/entities/Creation";
26
28
  import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
27
29
  import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
28
30
  import { CreationDetailScreen } from "./CreationDetailScreen";
31
+ import { CreationsProvider } from "../components/CreationsProvider";
29
32
 
30
33
  interface CreationsGalleryScreenProps {
31
34
  readonly userId: string | null;
@@ -40,8 +43,6 @@ interface CreationsGalleryScreenProps {
40
43
  readonly showFilter?: boolean;
41
44
  }
42
45
 
43
- import { CreationsProvider } from "../components/CreationsProvider";
44
-
45
46
  export function CreationsGalleryScreen(props: CreationsGalleryScreenProps) {
46
47
  return (
47
48
  <CreationsProvider config={props.config} t={props.t}>
@@ -62,6 +63,7 @@ function CreationsGalleryScreenContent({
62
63
  emptyActionLabel,
63
64
  showFilter = config.showFilter ?? true,
64
65
  }: CreationsGalleryScreenProps) {
66
+ const insets = useSafeAreaInsets();
65
67
  const tokens = useAppDesignTokens();
66
68
  const { share } = useSharing();
67
69
  const alert = useAlert();
@@ -76,14 +78,12 @@ function CreationsGalleryScreenContent({
76
78
  const deleteMutation = useDeleteCreation({ userId, repository });
77
79
  const { filtered, selectedIds, toggleFilter, clearFilters, isFiltered } = useCreationsFilter({ creations });
78
80
 
79
- // Refetch creations when screen comes into focus
80
81
  useFocusEffect(
81
82
  useCallback(() => {
82
83
  void refetch();
83
84
  }, [refetch])
84
85
  );
85
86
 
86
- // Prepare data for UI using utils
87
87
  const allCategories = useMemo(
88
88
  () => getFilterCategoriesFromConfig(config, t),
89
89
  [config, t],
@@ -118,27 +118,62 @@ function CreationsGalleryScreenContent({
118
118
  );
119
119
  }, [alert, config, deleteMutation, t]);
120
120
 
121
- // Handle viewing a creation - shows detail screen
122
121
  const handleView = useCallback((creation: Creation) => {
123
122
  setSelectedCreation(creation);
124
123
  }, []);
125
124
 
126
- // Handle favorite toggle
127
125
  const handleFavorite = useCallback((creation: Creation, isFavorite: boolean) => {
128
126
  void (async () => {
129
127
  if (!userId) return;
130
- const success = await repository.updateFavorite(
131
- userId,
132
- creation.id,
133
- isFavorite,
134
- );
128
+ const success = await repository.updateFavorite(userId, creation.id, isFavorite);
135
129
  if (success) {
136
130
  void refetch();
137
131
  }
138
132
  })();
139
133
  }, [userId, repository, refetch]);
140
134
 
141
- const renderEmptyComponent = useMemo(() => (
135
+ const styles = useStyles(tokens, insets);
136
+
137
+ const renderItem = useCallback(
138
+ ({ item }: { item: Creation }) => (
139
+ <CreationCard
140
+ creation={item}
141
+ callbacks={{
142
+ onPress: () => handleView(item),
143
+ onShare: async () => handleShare(item),
144
+ onDelete: () => handleDelete(item),
145
+ onFavorite: () => handleFavorite(item, !item.isFavorite),
146
+ }}
147
+ />
148
+ ),
149
+ [handleView, handleShare, handleDelete, handleFavorite]
150
+ );
151
+
152
+ const renderHeader = useMemo(() => {
153
+ if ((!creations || creations.length === 0) && !isLoading) return null;
154
+
155
+ return (
156
+ <View style={styles.header}>
157
+ <GalleryHeader
158
+ title={t(config.translations.title)}
159
+ count={filtered.length}
160
+ countLabel={t(config.translations.photoCount)}
161
+ isFiltered={isFiltered}
162
+ showFilter={showFilter}
163
+ filterLabel={t(config.translations.filterLabel)}
164
+ onFilterPress={() => {
165
+ if (__DEV__) {
166
+ // eslint-disable-next-line no-console
167
+ console.log('[CreationsGallery] Filter button pressed');
168
+ }
169
+ filterSheetRef.current?.present();
170
+ }}
171
+ />
172
+ </View>
173
+ );
174
+ }, [creations, isLoading, filtered.length, isFiltered, showFilter, t, config, styles.header]);
175
+
176
+ const renderEmpty = useMemo(() => (
142
177
  <GalleryEmptyStates
143
178
  isLoading={isLoading}
144
179
  creations={creations}
@@ -166,63 +201,37 @@ function CreationsGalleryScreenContent({
166
201
  }
167
202
 
168
203
  return (
169
- <>
170
- <ScreenLayout
171
- scrollable={false}
172
- edges={["top"]}
173
- backgroundColor={tokens.colors.background}
174
- header={
175
- (!creations || creations?.length === 0) && !isLoading ? null : (
176
- <GalleryHeader
177
- title={t(config.translations.title)}
178
- count={filtered.length}
179
- countLabel={t(config.translations.photoCount)}
180
- isFiltered={isFiltered}
181
- showFilter={showFilter}
182
- filterLabel={t(config.translations.filterLabel)}
183
- onFilterPress={() => {
184
- if (__DEV__) {
185
- // eslint-disable-next-line no-console
186
- console.log('[CreationsGallery] Filter button pressed');
187
- // eslint-disable-next-line no-console
188
- console.log('[CreationsGallery] filterSheetRef.current:', filterSheetRef.current);
189
- // eslint-disable-next-line no-console
190
- console.log('[CreationsGallery] allCategories:', allCategories);
191
- }
192
- filterSheetRef.current?.present();
193
- }}
194
- />
195
- )
204
+ <View style={[styles.container, { backgroundColor: tokens.colors.background }]}>
205
+ <FlatList
206
+ data={filtered}
207
+ renderItem={renderItem}
208
+ keyExtractor={(item) => item.id}
209
+ ListHeaderComponent={renderHeader}
210
+ ListEmptyComponent={renderEmpty}
211
+ contentContainerStyle={[
212
+ styles.listContent,
213
+ (!filtered || filtered.length === 0) && styles.emptyContent,
214
+ ]}
215
+ showsVerticalScrollIndicator={false}
216
+ refreshControl={
217
+ <RefreshControl
218
+ refreshing={isLoading}
219
+ onRefresh={() => void refetch()}
220
+ tintColor={tokens.colors.primary}
221
+ />
196
222
  }
197
- >
198
- {/* Main Content Grid - handles empty/loading via ListEmptyComponent */}
199
- <CreationsGrid
200
- creations={filtered}
201
- isLoading={isLoading}
202
- onRefresh={() => void refetch()}
203
- onPress={(creation) => handleView(creation as Creation)}
204
- onShare={async (creation) => handleShare(creation as Creation)}
205
- onDelete={(creation) => handleDelete(creation as Creation)}
206
- onFavorite={(creation) => {
207
- const c = creation as Creation;
208
- handleFavorite(c, !c.isFavorite);
209
- }}
210
- contentContainerStyle={{ paddingBottom: tokens.spacing.xl }}
211
- ListEmptyComponent={renderEmptyComponent}
212
- />
223
+ />
213
224
 
214
- <CreationImageViewer
215
- creations={filtered}
216
- visible={viewerVisible}
217
- index={viewerIndex}
218
- onDismiss={() => setViewerVisible(false)}
219
- onIndexChange={setViewerIndex}
220
- enableEditing={enableEditing}
221
- onImageEdit={onImageEdit}
222
- />
223
- </ScreenLayout>
225
+ <CreationImageViewer
226
+ creations={filtered}
227
+ visible={viewerVisible}
228
+ index={viewerIndex}
229
+ onDismiss={() => setViewerVisible(false)}
230
+ onIndexChange={setViewerIndex}
231
+ enableEditing={enableEditing}
232
+ onImageEdit={onImageEdit}
233
+ />
224
234
 
225
- {/* FilterBottomSheet must be outside ScreenLayout for proper portal rendering */}
226
235
  <FilterBottomSheet
227
236
  ref={filterSheetRef}
228
237
  categories={allCategories}
@@ -234,7 +243,27 @@ function CreationsGalleryScreenContent({
234
243
  onClearFilters={clearFilters}
235
244
  title={t(config.translations.filterTitle)}
236
245
  />
237
- </>
246
+ </View>
238
247
  );
239
248
  }
240
249
 
250
+ const useStyles = (tokens: DesignTokens, insets: { top: number; bottom: number }) =>
251
+ StyleSheet.create({
252
+ container: {
253
+ flex: 1,
254
+ },
255
+ header: {
256
+ paddingTop: insets.top + tokens.spacing.md,
257
+ backgroundColor: tokens.colors.surface,
258
+ borderBottomWidth: 1,
259
+ borderBottomColor: tokens.colors.border,
260
+ },
261
+ listContent: {
262
+ paddingHorizontal: tokens.spacing.md,
263
+ paddingTop: tokens.spacing.md,
264
+ paddingBottom: insets.bottom + 100,
265
+ },
266
+ emptyContent: {
267
+ flexGrow: 1,
268
+ },
269
+ });