@umituz/react-native-ai-generation-content 1.27.25 → 1.27.27

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.27.25",
3
+ "version": "1.27.27",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -16,6 +16,7 @@ import {
16
16
  } from "./wizard-strategy.constants";
17
17
  import { buildFacePreservationPrompt } from "../../../../prompts/infrastructure/builders/face-preservation-builder";
18
18
  import { buildInteractionStylePrompt, type InteractionStyle } from "../../../../prompts/infrastructure/builders/interaction-style-builder";
19
+ import { extractPrompt, extractSelection } from "../utils";
19
20
 
20
21
  declare const __DEV__: boolean;
21
22
 
@@ -165,9 +166,8 @@ export async function buildImageInput(
165
166
  ): Promise<ImageGenerationInput | null> {
166
167
  const photos = await extractPhotosFromWizardData(wizardData);
167
168
 
168
- // Get prompt from wizardData (text_input step) OR scenario.aiPrompt
169
- const userPrompt = wizardData.prompt as string | undefined;
170
- const prompt = userPrompt?.trim() || scenario.aiPrompt?.trim();
169
+ // Extract prompt using type-safe extractor with fallback
170
+ const prompt = extractPrompt(wizardData, scenario.aiPrompt);
171
171
 
172
172
  if (!prompt) {
173
173
  throw new Error("Prompt is required for image generation");
@@ -178,18 +178,19 @@ export async function buildImageInput(
178
178
  if (photos.length > 0) {
179
179
  const styleEnhancements: string[] = [];
180
180
 
181
- const romanticMoods = wizardData.selection_romantic_mood as string[] | undefined;
182
- if (romanticMoods?.length) {
181
+ // Extract selections using type-safe extractor
182
+ const romanticMoods = extractSelection(wizardData.selection_romantic_mood);
183
+ if (Array.isArray(romanticMoods) && romanticMoods.length > 0) {
183
184
  styleEnhancements.push(`Mood: ${romanticMoods.join(", ")}`);
184
185
  }
185
186
 
186
- const artStyle = wizardData.selection_art_style as string | undefined;
187
- if (artStyle && artStyle !== DEFAULT_STYLE_VALUE) {
187
+ const artStyle = extractSelection(wizardData.selection_art_style);
188
+ if (typeof artStyle === "string" && artStyle !== DEFAULT_STYLE_VALUE) {
188
189
  styleEnhancements.push(`Art style: ${artStyle}`);
189
190
  }
190
191
 
191
- const artist = wizardData.selection_artist_style as string | undefined;
192
- if (artist && artist !== DEFAULT_STYLE_VALUE) {
192
+ const artist = extractSelection(wizardData.selection_artist_style);
193
+ if (typeof artist === "string" && artist !== DEFAULT_STYLE_VALUE) {
193
194
  styleEnhancements.push(`Artist style: ${artist}`);
194
195
  }
195
196
 
@@ -198,8 +199,9 @@ export async function buildImageInput(
198
199
  }
199
200
  }
200
201
 
201
- // Get style from wizard selection (for text-to-image)
202
- const style = wizardData.style as string | undefined;
202
+ // Extract style using type-safe extractor (for text-to-image)
203
+ const styleValue = extractSelection(wizardData.style);
204
+ const style = typeof styleValue === "string" ? styleValue : undefined;
203
205
 
204
206
  // Get interaction style from scenario (default: romantic for couple apps)
205
207
  const interactionStyle = (scenario.interactionStyle as InteractionStyle) ?? "romantic";
@@ -10,6 +10,7 @@ import { createCreationsRepository } from "../../../../creations/infrastructure/
10
10
  import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
11
11
  import type { WizardStrategy } from "./wizard-strategy.types";
12
12
  import { PHOTO_KEY_PREFIX, VIDEO_FEATURE_PATTERNS } from "./wizard-strategy.constants";
13
+ import { extractPrompt, extractDuration } from "../utils";
13
14
 
14
15
  // ============================================================================
15
16
  // Types
@@ -86,17 +87,15 @@ export async function buildVideoInput(
86
87
  ): Promise<VideoGenerationInput | null> {
87
88
  const photos = await extractPhotosFromWizardData(wizardData);
88
89
 
89
- // Get prompt from wizardData or scenario
90
- const userPrompt = wizardData.prompt as string | undefined;
91
- const motionPrompt = wizardData.motion_prompt as string | undefined;
92
- const prompt = userPrompt?.trim() || motionPrompt?.trim() || scenario.aiPrompt?.trim();
90
+ // Extract prompt using type-safe extractor with fallback
91
+ const prompt = extractPrompt(wizardData, scenario.aiPrompt);
93
92
 
94
93
  if (!prompt) {
95
94
  throw new Error("Prompt is required for video generation");
96
95
  }
97
96
 
98
- // Get duration from wizardData
99
- const duration = wizardData.duration as number | undefined;
97
+ // Extract duration using type-safe extractor (default: 5 seconds)
98
+ const duration = extractDuration(wizardData, 5);
100
99
 
101
100
  return {
102
101
  sourceImageBase64: photos[0],
@@ -0,0 +1,8 @@
1
+ export {
2
+ extractString,
3
+ extractTrimmedString,
4
+ extractNumber,
5
+ extractSelection,
6
+ extractPrompt,
7
+ extractDuration,
8
+ } from "./wizard-data-extractors";
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Wizard Data Extractors
3
+ * Type-safe utilities for extracting values from wizard data
4
+ *
5
+ * Pattern: Type Guards + Normalizers
6
+ * @see https://www.typescriptlang.org/docs/handbook/2/narrowing.html
7
+ * @see https://betterstack.com/community/guides/scaling-nodejs/typescript-type-guards/
8
+ */
9
+
10
+ // ============================================================================
11
+ // Type Guards
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Check if value is a non-null object
16
+ */
17
+ function isObject(value: unknown): value is Record<string, unknown> {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19
+ }
20
+
21
+ /**
22
+ * Check if object has a specific property
23
+ * Uses 'in' operator for safe property checking
24
+ */
25
+ function hasProperty<K extends string>(
26
+ obj: Record<string, unknown>,
27
+ key: K,
28
+ ): obj is Record<K, unknown> {
29
+ return key in obj;
30
+ }
31
+
32
+ // ============================================================================
33
+ // String Extractors
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Extracts string from various input formats:
38
+ * - Direct string: "my text"
39
+ * - Object with text field: { text: "my text" }
40
+ * - Object with uri field (fallback): { uri: "my text" }
41
+ *
42
+ * @param value - The value to extract string from
43
+ * @returns The extracted string or undefined
44
+ */
45
+ export function extractString(value: unknown): string | undefined {
46
+ // Direct string
47
+ if (typeof value === "string") {
48
+ return value;
49
+ }
50
+
51
+ // Object with text or uri field
52
+ if (isObject(value)) {
53
+ if (hasProperty(value, "text") && typeof value.text === "string") {
54
+ return value.text;
55
+ }
56
+ if (hasProperty(value, "uri") && typeof value.uri === "string") {
57
+ return value.uri;
58
+ }
59
+ }
60
+
61
+ return undefined;
62
+ }
63
+
64
+ /**
65
+ * Extracts and trims string, returning undefined if empty
66
+ */
67
+ export function extractTrimmedString(value: unknown): string | undefined {
68
+ const str = extractString(value);
69
+ const trimmed = str?.trim();
70
+ return trimmed && trimmed.length > 0 ? trimmed : undefined;
71
+ }
72
+
73
+ // ============================================================================
74
+ // Number Extractors
75
+ // ============================================================================
76
+
77
+ /**
78
+ * Extracts number from various input formats:
79
+ * - Direct number: 5
80
+ * - Object with value field: { value: 5 }
81
+ * - Object with selection field: { selection: 5 }
82
+ *
83
+ * @param value - The value to extract number from
84
+ * @returns The extracted number or undefined
85
+ */
86
+ export function extractNumber(value: unknown): number | undefined {
87
+ // Direct number
88
+ if (typeof value === "number" && !Number.isNaN(value)) {
89
+ return value;
90
+ }
91
+
92
+ // Object with value or selection field
93
+ if (isObject(value)) {
94
+ if (hasProperty(value, "value") && typeof value.value === "number") {
95
+ return value.value;
96
+ }
97
+ if (hasProperty(value, "selection") && typeof value.selection === "number") {
98
+ return value.selection;
99
+ }
100
+ }
101
+
102
+ return undefined;
103
+ }
104
+
105
+ // ============================================================================
106
+ // Selection Extractors
107
+ // ============================================================================
108
+
109
+ /**
110
+ * Extracts selection value (string or string array) from wizard data
111
+ *
112
+ * @param value - The value to extract selection from
113
+ * @returns The extracted selection or undefined
114
+ */
115
+ export function extractSelection(value: unknown): string | string[] | undefined {
116
+ // Direct string
117
+ if (typeof value === "string") {
118
+ return value;
119
+ }
120
+
121
+ // Direct string array
122
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
123
+ return value as string[];
124
+ }
125
+
126
+ // Object with selection field
127
+ if (isObject(value)) {
128
+ if (hasProperty(value, "selection")) {
129
+ const selection = value.selection;
130
+ if (typeof selection === "string") {
131
+ return selection;
132
+ }
133
+ if (Array.isArray(selection) && selection.every((v) => typeof v === "string")) {
134
+ return selection as string[];
135
+ }
136
+ }
137
+ }
138
+
139
+ return undefined;
140
+ }
141
+
142
+ // ============================================================================
143
+ // Prompt Extractor (Specialized)
144
+ // ============================================================================
145
+
146
+ /**
147
+ * Extracts prompt from wizard data with fallback chain
148
+ * Checks multiple keys in order: prompt, motion_prompt, text
149
+ *
150
+ * @param wizardData - The wizard data object
151
+ * @param fallback - Optional fallback value (e.g., scenario.aiPrompt)
152
+ * @returns The extracted and trimmed prompt or undefined
153
+ */
154
+ export function extractPrompt(
155
+ wizardData: Record<string, unknown>,
156
+ fallback?: string,
157
+ ): string | undefined {
158
+ // Priority chain for prompt keys
159
+ const promptKeys = ["prompt", "motion_prompt", "text", "userPrompt"];
160
+
161
+ for (const key of promptKeys) {
162
+ if (key in wizardData) {
163
+ const extracted = extractTrimmedString(wizardData[key]);
164
+ if (extracted) {
165
+ return extracted;
166
+ }
167
+ }
168
+ }
169
+
170
+ // Use fallback if provided
171
+ return fallback?.trim() || undefined;
172
+ }
173
+
174
+ // ============================================================================
175
+ // Duration Extractor (Specialized)
176
+ // ============================================================================
177
+
178
+ /**
179
+ * Extracts duration from wizard data
180
+ * Handles both direct number and object with value field
181
+ *
182
+ * @param wizardData - The wizard data object
183
+ * @param defaultValue - Default duration if not found
184
+ * @returns The extracted duration in seconds
185
+ */
186
+ export function extractDuration(
187
+ wizardData: Record<string, unknown>,
188
+ defaultValue = 5,
189
+ ): number {
190
+ const durationData = wizardData.duration;
191
+
192
+ const extracted = extractNumber(durationData);
193
+ if (extracted !== undefined && extracted > 0) {
194
+ return extracted;
195
+ }
196
+
197
+ return defaultValue;
198
+ }
@@ -1,17 +1,19 @@
1
1
  /**
2
2
  * SelectionScreen
3
3
  * Generic selection step for wizard flows (duration, style, etc.)
4
+ * Uses design system: NavigationHeader + ScreenLayout
4
5
  */
5
6
 
6
- import React, { useState, useCallback } from "react";
7
- import { View, ScrollView, TouchableOpacity } from "react-native";
7
+ import React, { useState, useCallback, useMemo } from "react";
8
+ import { View, TouchableOpacity, StyleSheet } from "react-native";
8
9
  import {
9
10
  AtomicText,
10
- AtomicButton,
11
11
  AtomicIcon,
12
12
  useAppDesignTokens,
13
+ ScreenLayout,
14
+ NavigationHeader,
15
+ type DesignTokens,
13
16
  } from "@umituz/react-native-design-system";
14
- import { styles } from "./SelectionScreen.styles";
15
17
  import type { SelectionScreenProps } from "./SelectionScreen.types";
16
18
 
17
19
  export type {
@@ -78,28 +80,55 @@ export const SelectionScreen: React.FC<SelectionScreenProps> = ({
78
80
  [isMultiSelect, selected],
79
81
  );
80
82
 
81
- return (
82
- <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
83
- <View style={[styles.header, { paddingHorizontal: tokens.spacing.md }]}>
84
- <AtomicButton variant="text" size="sm" onPress={onBack}>
85
- <View style={styles.backButtonContent}>
86
- <AtomicIcon name="arrow-back" size="sm" color="textPrimary" />
87
- {translations.backButton ? (
88
- <AtomicText type="labelMedium" color="textPrimary" style={styles.backButtonText}>
89
- {translations.backButton}
90
- </AtomicText>
91
- ) : null}
92
- </View>
93
- </AtomicButton>
94
- </View>
83
+ const styles = useMemo(() => createStyles(tokens), [tokens]);
95
84
 
96
- <ScrollView style={styles.scrollView} contentContainerStyle={{ padding: tokens.spacing.md }}>
85
+ return (
86
+ <View style={{ flex: 1, backgroundColor: tokens.colors.backgroundPrimary }}>
87
+ <NavigationHeader
88
+ title=""
89
+ onBackPress={onBack}
90
+ rightElement={
91
+ <TouchableOpacity
92
+ onPress={handleContinue}
93
+ activeOpacity={0.7}
94
+ disabled={!canContinue}
95
+ style={[
96
+ styles.continueButton,
97
+ {
98
+ backgroundColor: canContinue ? tokens.colors.primary : tokens.colors.surfaceVariant,
99
+ opacity: canContinue ? 1 : 0.5,
100
+ },
101
+ ]}
102
+ >
103
+ <AtomicText
104
+ type="bodyMedium"
105
+ style={[
106
+ styles.continueText,
107
+ { color: canContinue ? tokens.colors.onPrimary : tokens.colors.textSecondary },
108
+ ]}
109
+ >
110
+ {translations.continueButton}
111
+ </AtomicText>
112
+ <AtomicIcon
113
+ name="arrow-forward"
114
+ size="sm"
115
+ color={canContinue ? "onPrimary" : "textSecondary"}
116
+ />
117
+ </TouchableOpacity>
118
+ }
119
+ />
120
+ <ScreenLayout
121
+ scrollable={true}
122
+ edges={["left", "right"]}
123
+ hideScrollIndicator={true}
124
+ contentContainerStyle={styles.scrollContent}
125
+ >
97
126
  <AtomicText type="headlineMedium" color="textPrimary" style={styles.title}>
98
127
  {translations.title}
99
128
  </AtomicText>
100
129
 
101
130
  {translations.subtitle ? (
102
- <AtomicText type="bodyMedium" color="textSecondary" style={{ marginBottom: tokens.spacing.lg }}>
131
+ <AtomicText type="bodyMedium" color="textSecondary" style={styles.subtitle}>
103
132
  {translations.subtitle}
104
133
  </AtomicText>
105
134
  ) : null}
@@ -115,7 +144,6 @@ export const SelectionScreen: React.FC<SelectionScreenProps> = ({
115
144
  {
116
145
  backgroundColor: isSelected ? tokens.colors.primaryContainer : tokens.colors.backgroundSecondary,
117
146
  borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
118
- borderRadius: tokens.borders.radius.md,
119
147
  },
120
148
  ]}
121
149
  onPress={() => handleSelect(option.id)}
@@ -136,13 +164,61 @@ export const SelectionScreen: React.FC<SelectionScreenProps> = ({
136
164
  );
137
165
  })}
138
166
  </View>
139
- </ScrollView>
140
-
141
- <View style={[styles.footer, { padding: tokens.spacing.md }]}>
142
- <AtomicButton variant="primary" size="lg" onPress={handleContinue} disabled={!canContinue} style={styles.continueButton}>
143
- {translations.continueButton}
144
- </AtomicButton>
145
- </View>
167
+ </ScreenLayout>
146
168
  </View>
147
169
  );
148
170
  };
171
+
172
+ const createStyles = (tokens: DesignTokens) =>
173
+ StyleSheet.create({
174
+ scrollContent: {
175
+ paddingHorizontal: tokens.spacing.lg,
176
+ paddingBottom: 40,
177
+ },
178
+ title: {
179
+ marginBottom: tokens.spacing.sm,
180
+ },
181
+ subtitle: {
182
+ marginBottom: tokens.spacing.lg,
183
+ },
184
+ optionsGrid: {
185
+ flexDirection: "row",
186
+ flexWrap: "wrap",
187
+ gap: tokens.spacing.sm,
188
+ },
189
+ optionCard: {
190
+ flex: 1,
191
+ minWidth: "45%",
192
+ padding: tokens.spacing.md,
193
+ borderWidth: 2,
194
+ borderRadius: tokens.borders.radius.md,
195
+ alignItems: "center",
196
+ justifyContent: "center",
197
+ gap: tokens.spacing.xs,
198
+ position: "relative",
199
+ },
200
+ optionLabel: {
201
+ textAlign: "center",
202
+ },
203
+ checkmark: {
204
+ position: "absolute",
205
+ top: tokens.spacing.xs,
206
+ right: tokens.spacing.xs,
207
+ width: 20,
208
+ height: 20,
209
+ borderRadius: 10,
210
+ alignItems: "center",
211
+ justifyContent: "center",
212
+ },
213
+ continueButton: {
214
+ flexDirection: "row",
215
+ alignItems: "center",
216
+ paddingHorizontal: tokens.spacing.md,
217
+ paddingVertical: tokens.spacing.xs,
218
+ borderRadius: tokens.borders.radius.full,
219
+ },
220
+ continueText: {
221
+ fontWeight: "800",
222
+ marginRight: 4,
223
+ },
224
+ });
@@ -1,16 +1,19 @@
1
1
  /**
2
2
  * TextInputScreen
3
3
  * Generic text input step for wizard flows
4
- * Header: Back on left, Continue on right
4
+ * Uses design system: NavigationHeader + ScreenLayout
5
5
  */
6
6
 
7
- import React, { useState, useCallback } from "react";
8
- import { View, ScrollView, TextInput, TouchableOpacity, StyleSheet } from "react-native";
7
+ import React, { useState, useCallback, useMemo } from "react";
8
+ import { View, TextInput, TouchableOpacity, StyleSheet } from "react-native";
9
9
  import {
10
10
  AtomicText,
11
11
  AtomicButton,
12
12
  AtomicIcon,
13
13
  useAppDesignTokens,
14
+ ScreenLayout,
15
+ NavigationHeader,
16
+ type DesignTokens,
14
17
  } from "@umituz/react-native-design-system";
15
18
  import type { TextInputScreenProps } from "./TextInputScreen.types";
16
19
 
@@ -46,64 +49,63 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
46
49
  setText(example);
47
50
  }, []);
48
51
 
49
- return (
50
- <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
51
- {/* Header with Back on left, Continue on right */}
52
- <View style={[styles.header, { paddingHorizontal: tokens.spacing.md }]}>
53
- <TouchableOpacity onPress={onBack} style={styles.backButton}>
54
- <AtomicIcon name="chevron-left" size="md" color="textPrimary" />
55
- </TouchableOpacity>
52
+ const styles = useMemo(() => createStyles(tokens), [tokens]);
56
53
 
57
- <TouchableOpacity
58
- onPress={handleContinue}
59
- disabled={!canContinue}
60
- style={[
61
- styles.continueButton,
62
- {
63
- backgroundColor: canContinue ? tokens.colors.primary : tokens.colors.surfaceSecondary,
64
- borderRadius: tokens.radius.md,
65
- },
66
- ]}
67
- >
68
- <AtomicText
69
- type="labelLarge"
70
- style={{
71
- color: canContinue ? tokens.colors.textInverse : tokens.colors.textSecondary,
72
- fontWeight: "600",
73
- }}
54
+ return (
55
+ <View style={{ flex: 1, backgroundColor: tokens.colors.backgroundPrimary }}>
56
+ <NavigationHeader
57
+ title=""
58
+ onBackPress={onBack}
59
+ rightElement={
60
+ <TouchableOpacity
61
+ onPress={handleContinue}
62
+ activeOpacity={0.7}
63
+ disabled={!canContinue}
64
+ style={[
65
+ styles.continueButton,
66
+ {
67
+ backgroundColor: canContinue ? tokens.colors.primary : tokens.colors.surfaceVariant,
68
+ opacity: canContinue ? 1 : 0.5,
69
+ },
70
+ ]}
74
71
  >
75
- {translations.continueButton}
76
- </AtomicText>
77
- </TouchableOpacity>
78
- </View>
79
-
80
- <ScrollView
81
- style={styles.scrollView}
82
- contentContainerStyle={{ padding: tokens.spacing.md }}
83
- keyboardShouldPersistTaps="handled"
72
+ <AtomicText
73
+ type="bodyMedium"
74
+ style={[
75
+ styles.continueText,
76
+ { color: canContinue ? tokens.colors.onPrimary : tokens.colors.textSecondary },
77
+ ]}
78
+ >
79
+ {translations.continueButton}
80
+ </AtomicText>
81
+ <AtomicIcon
82
+ name="arrow-forward"
83
+ size="sm"
84
+ color={canContinue ? "onPrimary" : "textSecondary"}
85
+ />
86
+ </TouchableOpacity>
87
+ }
88
+ />
89
+ <ScreenLayout
90
+ scrollable={true}
91
+ edges={["left", "right"]}
92
+ hideScrollIndicator={true}
93
+ keyboardAvoiding={true}
94
+ contentContainerStyle={styles.scrollContent}
84
95
  >
85
96
  <AtomicText type="headlineMedium" color="textPrimary" style={styles.title}>
86
97
  {translations.title}
87
98
  </AtomicText>
88
99
 
89
100
  {translations.subtitle ? (
90
- <AtomicText type="bodyMedium" color="textSecondary" style={{ marginBottom: tokens.spacing.lg }}>
101
+ <AtomicText type="bodyMedium" color="textSecondary" style={styles.subtitle}>
91
102
  {translations.subtitle}
92
103
  </AtomicText>
93
104
  ) : null}
94
105
 
95
- <View
96
- style={[
97
- styles.inputContainer,
98
- {
99
- backgroundColor: tokens.colors.backgroundSecondary,
100
- borderRadius: tokens.borders.radius.md,
101
- borderColor: tokens.colors.border,
102
- },
103
- ]}
104
- >
106
+ <View style={styles.inputContainer}>
105
107
  <TextInput
106
- style={[styles.textInput, { color: tokens.colors.textPrimary, minHeight: config?.multiline ? 120 : 48 }]}
108
+ style={[styles.textInput, { minHeight: config?.multiline ? 120 : 48 }]}
107
109
  placeholder={translations.placeholder}
108
110
  placeholderTextColor={tokens.colors.textTertiary}
109
111
  value={text}
@@ -118,8 +120,8 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
118
120
  </View>
119
121
 
120
122
  {examplePrompts.length > 0 && translations.examplesTitle ? (
121
- <View style={{ marginTop: tokens.spacing.lg }}>
122
- <AtomicText type="labelLarge" color="textSecondary" style={{ marginBottom: tokens.spacing.sm }}>
123
+ <View style={styles.examplesSection}>
124
+ <AtomicText type="labelLarge" color="textSecondary" style={styles.examplesTitle}>
123
125
  {translations.examplesTitle}
124
126
  </AtomicText>
125
127
  {examplePrompts.slice(0, 4).map((example, index) => (
@@ -128,53 +130,64 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
128
130
  variant="outline"
129
131
  size="sm"
130
132
  onPress={() => handleExampleSelect(example)}
131
- style={{ marginBottom: tokens.spacing.xs }}
133
+ style={styles.exampleButton}
132
134
  >
133
135
  {example.length > 50 ? `${example.slice(0, 50)}...` : example}
134
136
  </AtomicButton>
135
137
  ))}
136
138
  </View>
137
139
  ) : null}
138
- </ScrollView>
140
+ </ScreenLayout>
139
141
  </View>
140
142
  );
141
143
  };
142
144
 
143
- const styles = StyleSheet.create({
144
- container: {
145
- flex: 1,
146
- },
147
- header: {
148
- flexDirection: "row",
149
- justifyContent: "space-between",
150
- alignItems: "center",
151
- paddingVertical: 12,
152
- },
153
- backButton: {
154
- flexDirection: "row",
155
- alignItems: "center",
156
- gap: 4,
157
- },
158
- continueButton: {
159
- paddingHorizontal: 16,
160
- paddingVertical: 8,
161
- },
162
- scrollView: {
163
- flex: 1,
164
- },
165
- title: {
166
- marginBottom: 8,
167
- },
168
- inputContainer: {
169
- borderWidth: 1,
170
- padding: 12,
171
- },
172
- textInput: {
173
- fontSize: 16,
174
- lineHeight: 22,
175
- },
176
- charCount: {
177
- textAlign: "right",
178
- marginTop: 8,
179
- },
180
- });
145
+ const createStyles = (tokens: DesignTokens) =>
146
+ StyleSheet.create({
147
+ scrollContent: {
148
+ paddingHorizontal: tokens.spacing.lg,
149
+ paddingBottom: 40,
150
+ },
151
+ title: {
152
+ marginBottom: tokens.spacing.sm,
153
+ },
154
+ subtitle: {
155
+ marginBottom: tokens.spacing.lg,
156
+ },
157
+ inputContainer: {
158
+ backgroundColor: tokens.colors.backgroundSecondary,
159
+ borderRadius: tokens.borders.radius.md,
160
+ borderWidth: 1,
161
+ borderColor: tokens.colors.border,
162
+ padding: tokens.spacing.md,
163
+ },
164
+ textInput: {
165
+ fontSize: 16,
166
+ lineHeight: 22,
167
+ color: tokens.colors.textPrimary,
168
+ },
169
+ charCount: {
170
+ textAlign: "right",
171
+ marginTop: tokens.spacing.sm,
172
+ },
173
+ examplesSection: {
174
+ marginTop: tokens.spacing.lg,
175
+ },
176
+ examplesTitle: {
177
+ marginBottom: tokens.spacing.sm,
178
+ },
179
+ exampleButton: {
180
+ marginBottom: tokens.spacing.xs,
181
+ },
182
+ continueButton: {
183
+ flexDirection: "row",
184
+ alignItems: "center",
185
+ paddingHorizontal: tokens.spacing.md,
186
+ paddingVertical: tokens.spacing.xs,
187
+ borderRadius: tokens.borders.radius.full,
188
+ },
189
+ continueText: {
190
+ fontWeight: "800",
191
+ marginRight: 4,
192
+ },
193
+ });
@@ -1,63 +0,0 @@
1
- /**
2
- * SelectionScreen Styles
3
- */
4
-
5
- import { StyleSheet } from "react-native";
6
-
7
- export const styles = StyleSheet.create({
8
- container: {
9
- flex: 1,
10
- },
11
- header: {
12
- flexDirection: "row",
13
- alignItems: "center",
14
- paddingVertical: 8,
15
- },
16
- backButtonContent: {
17
- flexDirection: "row",
18
- alignItems: "center",
19
- },
20
- backButtonText: {
21
- marginLeft: 4,
22
- },
23
- scrollView: {
24
- flex: 1,
25
- },
26
- title: {
27
- marginBottom: 8,
28
- },
29
- optionsGrid: {
30
- flexDirection: "row",
31
- flexWrap: "wrap",
32
- gap: 12,
33
- },
34
- optionCard: {
35
- flex: 1,
36
- minWidth: "45%",
37
- padding: 16,
38
- borderWidth: 2,
39
- alignItems: "center",
40
- position: "relative",
41
- },
42
- optionLabel: {
43
- marginTop: 8,
44
- textAlign: "center",
45
- },
46
- checkmark: {
47
- position: "absolute",
48
- top: 8,
49
- right: 8,
50
- width: 20,
51
- height: 20,
52
- borderRadius: 10,
53
- alignItems: "center",
54
- justifyContent: "center",
55
- },
56
- footer: {
57
- borderTopWidth: 1,
58
- borderTopColor: "rgba(0,0,0,0.1)",
59
- },
60
- continueButton: {
61
- width: "100%",
62
- },
63
- });