@umituz/react-native-ai-generation-content 1.17.27 → 1.17.29
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 +1 -1
- package/src/domain/interfaces/app-services.interface.ts +122 -0
- package/src/domain/interfaces/index.ts +1 -0
- package/src/index.ts +36 -0
- package/src/infrastructure/config/app-services.config.ts +122 -0
- package/src/infrastructure/config/index.ts +16 -0
- package/src/presentation/components/index.ts +1 -0
- package/src/presentation/components/selectors/AspectRatioSelector.tsx +105 -0
- package/src/presentation/components/selectors/DurationSelector.tsx +98 -0
- package/src/presentation/components/selectors/StyleSelector.tsx +131 -0
- package/src/presentation/components/selectors/index.ts +18 -0
- package/src/presentation/components/selectors/types.ts +32 -0
package/package.json
CHANGED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Services Interface
|
|
3
|
+
* Defines contracts for app-specific services that package uses
|
|
4
|
+
* Apps implement these interfaces to provide their specific logic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Network service interface
|
|
9
|
+
* Handles network availability checks
|
|
10
|
+
*/
|
|
11
|
+
export interface INetworkService {
|
|
12
|
+
/**
|
|
13
|
+
* Check if device is online
|
|
14
|
+
*/
|
|
15
|
+
isOnline: () => boolean;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Require network connection - throws if offline
|
|
19
|
+
*/
|
|
20
|
+
requireNetwork: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Credit service interface
|
|
25
|
+
* Handles credit operations (check, deduct, refund)
|
|
26
|
+
*/
|
|
27
|
+
export interface ICreditService {
|
|
28
|
+
/**
|
|
29
|
+
* Check if user has enough credits
|
|
30
|
+
* @param cost - Required credit amount
|
|
31
|
+
* @returns true if has enough credits
|
|
32
|
+
*/
|
|
33
|
+
checkCredits: (cost: number) => Promise<boolean>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Deduct credits from user balance
|
|
37
|
+
* @param cost - Amount to deduct
|
|
38
|
+
*/
|
|
39
|
+
deductCredits: (cost: number) => Promise<void>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Refund credits on failure (non-user caused errors)
|
|
43
|
+
* @param amount - Amount to refund
|
|
44
|
+
* @param error - Original error (used to determine if refund is applicable)
|
|
45
|
+
*/
|
|
46
|
+
refundCredits: (amount: number, error?: unknown) => Promise<void>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calculate credit cost for operation
|
|
50
|
+
* @param capability - Generation capability type
|
|
51
|
+
* @param metadata - Additional metadata for cost calculation
|
|
52
|
+
*/
|
|
53
|
+
calculateCost: (
|
|
54
|
+
capability: string,
|
|
55
|
+
metadata?: Record<string, unknown>,
|
|
56
|
+
) => number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Paywall service interface
|
|
61
|
+
* Shows paywall UI when credits insufficient
|
|
62
|
+
*/
|
|
63
|
+
export interface IPaywallService {
|
|
64
|
+
/**
|
|
65
|
+
* Show paywall to user
|
|
66
|
+
* @param requiredCredits - Credits needed for operation
|
|
67
|
+
*/
|
|
68
|
+
showPaywall: (requiredCredits: number) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Auth service interface
|
|
73
|
+
* Handles user authentication
|
|
74
|
+
*/
|
|
75
|
+
export interface IAuthService {
|
|
76
|
+
/**
|
|
77
|
+
* Get current user ID
|
|
78
|
+
* @returns User ID or null if not authenticated
|
|
79
|
+
*/
|
|
80
|
+
getUserId: () => string | null;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if user is authenticated
|
|
84
|
+
*/
|
|
85
|
+
isAuthenticated: () => boolean;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Require authenticated user - throws if not authenticated
|
|
89
|
+
* @returns User ID
|
|
90
|
+
*/
|
|
91
|
+
requireAuth: () => string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Analytics service interface (optional)
|
|
96
|
+
* Tracks events for analytics
|
|
97
|
+
*/
|
|
98
|
+
export interface IAnalyticsService {
|
|
99
|
+
/**
|
|
100
|
+
* Track an event
|
|
101
|
+
* @param event - Event name
|
|
102
|
+
* @param data - Event data
|
|
103
|
+
*/
|
|
104
|
+
track: (event: string, data: Record<string, unknown>) => void;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Combined app services interface
|
|
109
|
+
* Apps implement this to provide all required services
|
|
110
|
+
*/
|
|
111
|
+
export interface IAppServices {
|
|
112
|
+
readonly network: INetworkService;
|
|
113
|
+
readonly credits: ICreditService;
|
|
114
|
+
readonly paywall: IPaywallService;
|
|
115
|
+
readonly auth: IAuthService;
|
|
116
|
+
readonly analytics?: IAnalyticsService;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Partial app services for optional configuration
|
|
121
|
+
*/
|
|
122
|
+
export type PartialAppServices = Partial<IAppServices>;
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,14 @@ export type {
|
|
|
29
29
|
VideoFeatureType,
|
|
30
30
|
ImageFeatureInputData,
|
|
31
31
|
VideoFeatureInputData,
|
|
32
|
+
// App Services Interfaces
|
|
33
|
+
INetworkService,
|
|
34
|
+
ICreditService,
|
|
35
|
+
IPaywallService,
|
|
36
|
+
IAuthService,
|
|
37
|
+
IAnalyticsService,
|
|
38
|
+
IAppServices,
|
|
39
|
+
PartialAppServices,
|
|
32
40
|
} from "./domain/interfaces";
|
|
33
41
|
|
|
34
42
|
export {
|
|
@@ -85,6 +93,23 @@ export {
|
|
|
85
93
|
getPromptRequiredModes,
|
|
86
94
|
} from "./domain/constants/processing-modes.constants";
|
|
87
95
|
|
|
96
|
+
// =============================================================================
|
|
97
|
+
// INFRASTRUCTURE LAYER - App Services Configuration
|
|
98
|
+
// =============================================================================
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
configureAppServices,
|
|
102
|
+
updateAppServices,
|
|
103
|
+
getAppServices,
|
|
104
|
+
isAppServicesConfigured,
|
|
105
|
+
resetAppServices,
|
|
106
|
+
getNetworkService,
|
|
107
|
+
getCreditService,
|
|
108
|
+
getPaywallService,
|
|
109
|
+
getAuthService,
|
|
110
|
+
getAnalyticsService,
|
|
111
|
+
} from "./infrastructure/config";
|
|
112
|
+
|
|
88
113
|
// =============================================================================
|
|
89
114
|
// INFRASTRUCTURE LAYER - Services
|
|
90
115
|
// =============================================================================
|
|
@@ -250,6 +275,10 @@ export {
|
|
|
250
275
|
FeatureHeader,
|
|
251
276
|
// Photo Upload
|
|
252
277
|
PhotoUploadCard,
|
|
278
|
+
// Selectors
|
|
279
|
+
StyleSelector,
|
|
280
|
+
AspectRatioSelector,
|
|
281
|
+
DurationSelector,
|
|
253
282
|
} from "./presentation/components";
|
|
254
283
|
|
|
255
284
|
export type {
|
|
@@ -289,6 +318,13 @@ export type {
|
|
|
289
318
|
// Photo Upload
|
|
290
319
|
PhotoUploadCardProps,
|
|
291
320
|
PhotoUploadCardConfig,
|
|
321
|
+
// Selectors
|
|
322
|
+
StyleSelectorProps,
|
|
323
|
+
AspectRatioSelectorProps,
|
|
324
|
+
DurationSelectorProps,
|
|
325
|
+
StyleOption,
|
|
326
|
+
AspectRatioOption,
|
|
327
|
+
DurationValue,
|
|
292
328
|
} from "./presentation/components";
|
|
293
329
|
|
|
294
330
|
// =============================================================================
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Services Configuration
|
|
3
|
+
* Singleton for storing app-provided service implementations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IAppServices, PartialAppServices } from "../../domain/interfaces/app-services.interface";
|
|
7
|
+
|
|
8
|
+
declare const __DEV__: boolean;
|
|
9
|
+
|
|
10
|
+
let appServices: IAppServices | null = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Default no-op implementations for optional services
|
|
14
|
+
*/
|
|
15
|
+
const defaultAnalytics = {
|
|
16
|
+
track: () => {
|
|
17
|
+
// No-op by default
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Configure app services
|
|
23
|
+
* Must be called before using any generation features
|
|
24
|
+
* @param services - App-specific service implementations
|
|
25
|
+
*/
|
|
26
|
+
export function configureAppServices(services: IAppServices): void {
|
|
27
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console.log("[AppServices] Configuring app services");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
appServices = {
|
|
33
|
+
...services,
|
|
34
|
+
analytics: services.analytics || defaultAnalytics,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Update specific app services
|
|
40
|
+
* Useful for lazy initialization
|
|
41
|
+
* @param updates - Partial service updates
|
|
42
|
+
*/
|
|
43
|
+
export function updateAppServices(updates: PartialAppServices): void {
|
|
44
|
+
if (!appServices) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"[AppServices] Must call configureAppServices before updateAppServices",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
appServices = {
|
|
51
|
+
...appServices,
|
|
52
|
+
...updates,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get configured app services
|
|
58
|
+
* @throws Error if services not configured
|
|
59
|
+
*/
|
|
60
|
+
export function getAppServices(): IAppServices {
|
|
61
|
+
if (!appServices) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"[AppServices] App services not configured. Call configureAppServices first.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return appServices;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if app services are configured
|
|
72
|
+
*/
|
|
73
|
+
export function isAppServicesConfigured(): boolean {
|
|
74
|
+
return appServices !== null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reset app services (useful for testing)
|
|
79
|
+
*/
|
|
80
|
+
export function resetAppServices(): void {
|
|
81
|
+
appServices = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get network service
|
|
86
|
+
* @throws Error if not configured
|
|
87
|
+
*/
|
|
88
|
+
export function getNetworkService(): IAppServices["network"] {
|
|
89
|
+
return getAppServices().network;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get credit service
|
|
94
|
+
* @throws Error if not configured
|
|
95
|
+
*/
|
|
96
|
+
export function getCreditService(): IAppServices["credits"] {
|
|
97
|
+
return getAppServices().credits;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get paywall service
|
|
102
|
+
* @throws Error if not configured
|
|
103
|
+
*/
|
|
104
|
+
export function getPaywallService(): IAppServices["paywall"] {
|
|
105
|
+
return getAppServices().paywall;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get auth service
|
|
110
|
+
* @throws Error if not configured
|
|
111
|
+
*/
|
|
112
|
+
export function getAuthService(): IAppServices["auth"] {
|
|
113
|
+
return getAppServices().auth;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get analytics service
|
|
118
|
+
* @throws Error if not configured
|
|
119
|
+
*/
|
|
120
|
+
export function getAnalyticsService(): IAppServices["analytics"] {
|
|
121
|
+
return getAppServices().analytics;
|
|
122
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Config Exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
configureAppServices,
|
|
7
|
+
updateAppServices,
|
|
8
|
+
getAppServices,
|
|
9
|
+
isAppServicesConfigured,
|
|
10
|
+
resetAppServices,
|
|
11
|
+
getNetworkService,
|
|
12
|
+
getCreditService,
|
|
13
|
+
getPaywallService,
|
|
14
|
+
getAuthService,
|
|
15
|
+
getAnalyticsService,
|
|
16
|
+
} from "./app-services.config";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aspect Ratio Selector Component
|
|
3
|
+
* Generic, props-driven aspect ratio selection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
import type { AspectRatioOption } from "./types";
|
|
14
|
+
|
|
15
|
+
export interface AspectRatioSelectorProps {
|
|
16
|
+
ratios: AspectRatioOption[];
|
|
17
|
+
selectedRatio: "16:9" | "9:16" | "1:1";
|
|
18
|
+
onRatioSelect: (ratio: "16:9" | "9:16" | "1:1") => void;
|
|
19
|
+
title: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
|
|
23
|
+
ratios,
|
|
24
|
+
selectedRatio,
|
|
25
|
+
onRatioSelect,
|
|
26
|
+
title,
|
|
27
|
+
}) => {
|
|
28
|
+
const tokens = useAppDesignTokens();
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View style={componentStyles.section}>
|
|
32
|
+
<AtomicText
|
|
33
|
+
type="bodyMedium"
|
|
34
|
+
style={{
|
|
35
|
+
color: tokens.colors.textPrimary,
|
|
36
|
+
fontWeight: "600",
|
|
37
|
+
marginBottom: 12,
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
{title}
|
|
41
|
+
</AtomicText>
|
|
42
|
+
<View style={componentStyles.aspectRatioGrid}>
|
|
43
|
+
{ratios.map((ratio) => (
|
|
44
|
+
<TouchableOpacity
|
|
45
|
+
key={ratio.id}
|
|
46
|
+
style={[
|
|
47
|
+
componentStyles.aspectRatioCard,
|
|
48
|
+
{
|
|
49
|
+
backgroundColor:
|
|
50
|
+
selectedRatio === ratio.id
|
|
51
|
+
? tokens.colors.primary + "20"
|
|
52
|
+
: tokens.colors.surface,
|
|
53
|
+
borderColor:
|
|
54
|
+
selectedRatio === ratio.id
|
|
55
|
+
? tokens.colors.primary
|
|
56
|
+
: tokens.colors.borderLight,
|
|
57
|
+
},
|
|
58
|
+
]}
|
|
59
|
+
onPress={() => onRatioSelect(ratio.id)}
|
|
60
|
+
>
|
|
61
|
+
<AtomicIcon
|
|
62
|
+
name={ratio.icon as never}
|
|
63
|
+
size="lg"
|
|
64
|
+
color={selectedRatio === ratio.id ? "primary" : "secondary"}
|
|
65
|
+
/>
|
|
66
|
+
<AtomicText
|
|
67
|
+
type="bodySmall"
|
|
68
|
+
style={{
|
|
69
|
+
color: tokens.colors.textPrimary,
|
|
70
|
+
fontWeight: selectedRatio === ratio.id ? "600" : "400",
|
|
71
|
+
marginTop: 8,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
{ratio.name}
|
|
75
|
+
</AtomicText>
|
|
76
|
+
<AtomicText
|
|
77
|
+
type="labelSmall"
|
|
78
|
+
style={{ color: tokens.colors.textSecondary, marginTop: 2 }}
|
|
79
|
+
>
|
|
80
|
+
{ratio.description}
|
|
81
|
+
</AtomicText>
|
|
82
|
+
</TouchableOpacity>
|
|
83
|
+
))}
|
|
84
|
+
</View>
|
|
85
|
+
</View>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const componentStyles = StyleSheet.create({
|
|
90
|
+
section: {
|
|
91
|
+
padding: 16,
|
|
92
|
+
marginBottom: 8,
|
|
93
|
+
},
|
|
94
|
+
aspectRatioGrid: {
|
|
95
|
+
flexDirection: "row",
|
|
96
|
+
gap: 12,
|
|
97
|
+
},
|
|
98
|
+
aspectRatioCard: {
|
|
99
|
+
flex: 1,
|
|
100
|
+
padding: 16,
|
|
101
|
+
borderRadius: 12,
|
|
102
|
+
borderWidth: 2,
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
},
|
|
105
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duration Selector Component
|
|
3
|
+
* Generic, props-driven duration selection for video/audio generation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
} from "@umituz/react-native-design-system";
|
|
12
|
+
import type { DurationValue } from "./types";
|
|
13
|
+
|
|
14
|
+
export interface DurationSelectorProps<T extends DurationValue> {
|
|
15
|
+
duration: T;
|
|
16
|
+
durationOptions: readonly T[];
|
|
17
|
+
onDurationSelect: (duration: T) => void;
|
|
18
|
+
title: string;
|
|
19
|
+
formatLabel?: (duration: T) => string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function DurationSelector<T extends DurationValue>({
|
|
23
|
+
duration,
|
|
24
|
+
durationOptions,
|
|
25
|
+
onDurationSelect,
|
|
26
|
+
title,
|
|
27
|
+
formatLabel = (d) => `${d}s`,
|
|
28
|
+
}: DurationSelectorProps<T>): React.ReactElement {
|
|
29
|
+
const tokens = useAppDesignTokens();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<View style={componentStyles.section}>
|
|
33
|
+
<AtomicText
|
|
34
|
+
type="bodyMedium"
|
|
35
|
+
style={{
|
|
36
|
+
color: tokens.colors.textPrimary,
|
|
37
|
+
fontWeight: "600",
|
|
38
|
+
marginBottom: 12,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
{title}
|
|
42
|
+
</AtomicText>
|
|
43
|
+
<View style={componentStyles.durationGrid}>
|
|
44
|
+
{durationOptions.map((sec) => (
|
|
45
|
+
<TouchableOpacity
|
|
46
|
+
key={sec}
|
|
47
|
+
style={[
|
|
48
|
+
componentStyles.durationButton,
|
|
49
|
+
{
|
|
50
|
+
backgroundColor:
|
|
51
|
+
duration === sec
|
|
52
|
+
? tokens.colors.primary
|
|
53
|
+
: tokens.colors.surface,
|
|
54
|
+
borderColor:
|
|
55
|
+
duration === sec
|
|
56
|
+
? tokens.colors.primary
|
|
57
|
+
: tokens.colors.borderLight,
|
|
58
|
+
},
|
|
59
|
+
]}
|
|
60
|
+
onPress={() => onDurationSelect(sec)}
|
|
61
|
+
>
|
|
62
|
+
<AtomicText
|
|
63
|
+
type="bodyLarge"
|
|
64
|
+
style={{
|
|
65
|
+
color:
|
|
66
|
+
duration === sec
|
|
67
|
+
? tokens.colors.textInverse
|
|
68
|
+
: tokens.colors.textPrimary,
|
|
69
|
+
fontWeight: duration === sec ? "700" : "400",
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{formatLabel(sec)}
|
|
73
|
+
</AtomicText>
|
|
74
|
+
</TouchableOpacity>
|
|
75
|
+
))}
|
|
76
|
+
</View>
|
|
77
|
+
</View>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const componentStyles = StyleSheet.create({
|
|
82
|
+
section: {
|
|
83
|
+
padding: 16,
|
|
84
|
+
marginBottom: 8,
|
|
85
|
+
},
|
|
86
|
+
durationGrid: {
|
|
87
|
+
flexDirection: "row",
|
|
88
|
+
gap: 12,
|
|
89
|
+
},
|
|
90
|
+
durationButton: {
|
|
91
|
+
flex: 1,
|
|
92
|
+
paddingVertical: 20,
|
|
93
|
+
borderRadius: 12,
|
|
94
|
+
borderWidth: 2,
|
|
95
|
+
alignItems: "center",
|
|
96
|
+
justifyContent: "center",
|
|
97
|
+
},
|
|
98
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style Selector Component
|
|
3
|
+
* Generic, props-driven style selection for AI generation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
import type { StyleOption } from "./types";
|
|
14
|
+
|
|
15
|
+
export interface StyleSelectorProps {
|
|
16
|
+
styles: StyleOption[];
|
|
17
|
+
selectedStyle: string;
|
|
18
|
+
onStyleSelect: (styleId: string) => void;
|
|
19
|
+
title: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const StyleSelector: React.FC<StyleSelectorProps> = ({
|
|
23
|
+
styles,
|
|
24
|
+
selectedStyle,
|
|
25
|
+
onStyleSelect,
|
|
26
|
+
title,
|
|
27
|
+
}) => {
|
|
28
|
+
const tokens = useAppDesignTokens();
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View style={componentStyles.section}>
|
|
32
|
+
<AtomicText
|
|
33
|
+
type="bodyMedium"
|
|
34
|
+
style={{
|
|
35
|
+
color: tokens.colors.textPrimary,
|
|
36
|
+
fontWeight: "600",
|
|
37
|
+
marginBottom: 12,
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
{title}
|
|
41
|
+
</AtomicText>
|
|
42
|
+
<ScrollView
|
|
43
|
+
horizontal
|
|
44
|
+
showsHorizontalScrollIndicator={false}
|
|
45
|
+
style={componentStyles.stylesScroll}
|
|
46
|
+
>
|
|
47
|
+
{styles.map((style) => {
|
|
48
|
+
const isSelected = selectedStyle === style.id;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<TouchableOpacity
|
|
52
|
+
key={style.id}
|
|
53
|
+
style={[
|
|
54
|
+
componentStyles.styleCard,
|
|
55
|
+
{
|
|
56
|
+
backgroundColor: isSelected
|
|
57
|
+
? tokens.colors.primary
|
|
58
|
+
: tokens.colors.surface,
|
|
59
|
+
borderColor: isSelected
|
|
60
|
+
? tokens.colors.primary
|
|
61
|
+
: tokens.colors.borderLight,
|
|
62
|
+
},
|
|
63
|
+
]}
|
|
64
|
+
onPress={() => onStyleSelect(style.id)}
|
|
65
|
+
>
|
|
66
|
+
{style.thumbnail ? (
|
|
67
|
+
<AtomicText type="headlineLarge" style={{ marginBottom: 8 }}>
|
|
68
|
+
{style.thumbnail}
|
|
69
|
+
</AtomicText>
|
|
70
|
+
) : style.icon ? (
|
|
71
|
+
<AtomicIcon
|
|
72
|
+
name={style.icon as never}
|
|
73
|
+
size="lg"
|
|
74
|
+
color={isSelected ? "primary" : "secondary"}
|
|
75
|
+
/>
|
|
76
|
+
) : null}
|
|
77
|
+
<AtomicText
|
|
78
|
+
type="bodySmall"
|
|
79
|
+
style={{
|
|
80
|
+
color: isSelected
|
|
81
|
+
? tokens.colors.textInverse
|
|
82
|
+
: tokens.colors.textPrimary,
|
|
83
|
+
fontWeight: isSelected ? "600" : "400",
|
|
84
|
+
textAlign: "center",
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{style.name}
|
|
88
|
+
</AtomicText>
|
|
89
|
+
{style.description && (
|
|
90
|
+
<AtomicText
|
|
91
|
+
type="labelSmall"
|
|
92
|
+
style={{
|
|
93
|
+
color: isSelected
|
|
94
|
+
? tokens.colors.textInverse
|
|
95
|
+
: tokens.colors.textSecondary,
|
|
96
|
+
opacity: isSelected ? 0.9 : 0.7,
|
|
97
|
+
textAlign: "center",
|
|
98
|
+
marginTop: 4,
|
|
99
|
+
}}
|
|
100
|
+
numberOfLines={2}
|
|
101
|
+
>
|
|
102
|
+
{style.description}
|
|
103
|
+
</AtomicText>
|
|
104
|
+
)}
|
|
105
|
+
</TouchableOpacity>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</ScrollView>
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const componentStyles = StyleSheet.create({
|
|
114
|
+
section: {
|
|
115
|
+
padding: 16,
|
|
116
|
+
marginBottom: 8,
|
|
117
|
+
},
|
|
118
|
+
stylesScroll: {
|
|
119
|
+
marginHorizontal: -16,
|
|
120
|
+
paddingHorizontal: 16,
|
|
121
|
+
},
|
|
122
|
+
styleCard: {
|
|
123
|
+
width: 120,
|
|
124
|
+
padding: 16,
|
|
125
|
+
borderRadius: 16,
|
|
126
|
+
borderWidth: 2,
|
|
127
|
+
marginRight: 12,
|
|
128
|
+
alignItems: "center",
|
|
129
|
+
minHeight: 140,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selector Components
|
|
3
|
+
* Generic, props-driven selection UI components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { StyleSelector } from "./StyleSelector";
|
|
7
|
+
export { AspectRatioSelector } from "./AspectRatioSelector";
|
|
8
|
+
export { DurationSelector } from "./DurationSelector";
|
|
9
|
+
|
|
10
|
+
export type { StyleSelectorProps } from "./StyleSelector";
|
|
11
|
+
export type { AspectRatioSelectorProps } from "./AspectRatioSelector";
|
|
12
|
+
export type { DurationSelectorProps } from "./DurationSelector";
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
StyleOption,
|
|
16
|
+
AspectRatioOption,
|
|
17
|
+
DurationValue,
|
|
18
|
+
} from "./types";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selector Component Types
|
|
3
|
+
* Generic types for selector UI components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Style option for StyleSelector
|
|
8
|
+
* All fields are required - app provides translated values
|
|
9
|
+
*/
|
|
10
|
+
export interface StyleOption {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
thumbnail?: string;
|
|
15
|
+
icon?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Aspect ratio option for AspectRatioSelector
|
|
20
|
+
* App provides translated name and description
|
|
21
|
+
*/
|
|
22
|
+
export interface AspectRatioOption {
|
|
23
|
+
id: "16:9" | "9:16" | "1:1";
|
|
24
|
+
name: string;
|
|
25
|
+
icon: string;
|
|
26
|
+
description: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Duration value type (seconds)
|
|
31
|
+
*/
|
|
32
|
+
export type DurationValue = number;
|