@umituz/react-native-settings 4.17.14 → 4.17.16
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 +16 -15
- package/src/domains/about/__tests__/integration.test.tsx +328 -0
- package/src/domains/about/__tests__/types.d.ts +5 -0
- package/src/domains/about/domain/entities/AppInfo.ts +74 -0
- package/src/domains/about/domain/entities/__tests__/AppInfo.test.ts +93 -0
- package/src/domains/about/domain/repositories/IAboutRepository.ts +22 -0
- package/src/domains/about/index.ts +10 -0
- package/src/domains/about/infrastructure/repositories/AboutRepository.ts +68 -0
- package/src/domains/about/infrastructure/repositories/__tests__/AboutRepository.test.ts +153 -0
- package/src/domains/about/presentation/components/AboutContent.tsx +104 -0
- package/src/domains/about/presentation/components/AboutHeader.tsx +79 -0
- package/src/domains/about/presentation/components/AboutSection.tsx +134 -0
- package/src/domains/about/presentation/components/AboutSettingItem.tsx +208 -0
- package/src/domains/about/presentation/components/__tests__/AboutContent.simple.test.tsx +178 -0
- package/src/domains/about/presentation/components/__tests__/AboutContent.test.tsx +293 -0
- package/src/domains/about/presentation/components/__tests__/AboutHeader.test.tsx +201 -0
- package/src/domains/about/presentation/components/__tests__/AboutSettingItem.test.tsx +71 -0
- package/src/domains/about/presentation/hooks/__tests__/useAboutInfo.simple.test.tsx +229 -0
- package/src/domains/about/presentation/hooks/__tests__/useAboutInfo.test.tsx +240 -0
- package/src/domains/about/presentation/hooks/useAboutInfo.ts +262 -0
- package/src/domains/about/presentation/screens/AboutScreen.tsx +195 -0
- package/src/domains/about/presentation/screens/__tests__/AboutScreen.simple.test.tsx +199 -0
- package/src/domains/about/presentation/screens/__tests__/AboutScreen.test.tsx +366 -0
- package/src/domains/about/types/global.d.ts +15 -0
- package/src/domains/about/utils/__tests__/index.test.ts +408 -0
- package/src/domains/about/utils/index.ts +160 -0
- package/src/domains/appearance/__tests__/components/AppearanceScreen.test.tsx +195 -0
- package/src/domains/appearance/__tests__/hooks/index.test.tsx +232 -0
- package/src/domains/appearance/__tests__/integration/index.test.tsx +207 -0
- package/src/domains/appearance/__tests__/services/appearanceService.test.ts +299 -0
- package/src/domains/appearance/__tests__/setup.ts +96 -0
- package/src/domains/appearance/__tests__/stores/appearanceStore.test.tsx +175 -0
- package/src/domains/appearance/data/colorPalettes.ts +94 -0
- package/src/domains/appearance/hooks/index.ts +6 -0
- package/src/domains/appearance/hooks/useAppearance.ts +61 -0
- package/src/domains/appearance/hooks/useAppearanceActions.ts +144 -0
- package/src/domains/appearance/index.ts +7 -0
- package/src/domains/appearance/infrastructure/services/appearanceService.ts +301 -0
- package/src/domains/appearance/infrastructure/services/systemThemeDetection.ts +79 -0
- package/src/domains/appearance/infrastructure/services/validation.ts +91 -0
- package/src/domains/appearance/infrastructure/storage/appearanceStorage.ts +120 -0
- package/src/domains/appearance/infrastructure/stores/appearanceStore.ts +132 -0
- package/src/domains/appearance/presentation/components/AppearanceHeader.tsx +67 -0
- package/src/domains/appearance/presentation/components/AppearancePreview.tsx +141 -0
- package/src/domains/appearance/presentation/components/AppearanceSection.tsx +139 -0
- package/src/domains/appearance/presentation/components/ColorPicker.tsx +113 -0
- package/src/domains/appearance/presentation/components/CustomColorsSection.tsx +186 -0
- package/src/domains/appearance/presentation/components/ThemeModeSection.tsx +110 -0
- package/src/domains/appearance/presentation/components/ThemeOption.tsx +138 -0
- package/src/domains/appearance/presentation/components/index.ts +6 -0
- package/src/domains/appearance/presentation/screens/AppearanceScreen.tsx +226 -0
- package/src/domains/appearance/presentation/screens/index.ts +2 -0
- package/src/domains/appearance/types/index.ts +54 -0
- package/src/domains/faqs/domain/entities/FAQEntity.ts +16 -0
- package/src/domains/faqs/domain/services/FAQSearchService.ts +36 -0
- package/src/domains/faqs/domain/services/index.ts +1 -0
- package/src/domains/faqs/index.ts +7 -0
- package/src/domains/faqs/presentation/components/FAQCategory.tsx +71 -0
- package/src/domains/faqs/presentation/components/FAQEmptyState.tsx +75 -0
- package/src/domains/faqs/presentation/components/FAQItem.tsx +103 -0
- package/src/domains/faqs/presentation/components/FAQSearchBar.tsx +70 -0
- package/src/domains/faqs/presentation/components/FAQSection.tsx +50 -0
- package/src/domains/faqs/presentation/components/index.ts +18 -0
- package/src/domains/faqs/presentation/hooks/index.ts +6 -0
- package/src/domains/faqs/presentation/hooks/useFAQExpansion.ts +51 -0
- package/src/domains/faqs/presentation/hooks/useFAQSearch.ts +33 -0
- package/src/domains/faqs/presentation/screens/FAQScreen.tsx +129 -0
- package/src/domains/faqs/presentation/screens/index.ts +2 -0
- package/src/domains/feedback/domain/entities/FeedbackEntity.ts +92 -0
- package/src/domains/feedback/domain/repositories/IFeedbackRepository.ts +28 -0
- package/src/domains/feedback/index.ts +6 -0
- package/src/domains/feedback/presentation/components/FeedbackForm.tsx +189 -0
- package/src/domains/feedback/presentation/components/FeedbackModal.tsx +111 -0
- package/src/domains/feedback/presentation/components/SupportSection.tsx +160 -0
- package/src/domains/feedback/presentation/hooks/useDeleteFeedback.ts +25 -0
- package/src/domains/feedback/presentation/hooks/useFeedbackForm.ts +59 -0
- package/src/domains/feedback/presentation/hooks/useSubmitFeedback.ts +55 -0
- package/src/domains/feedback/presentation/hooks/useUserFeedback.ts +29 -0
- package/src/domains/legal/__tests__/ContentValidationService.test.ts +195 -0
- package/src/domains/legal/__tests__/StyleCacheService.test.ts +110 -0
- package/src/domains/legal/__tests__/UrlHandlerService.test.ts +71 -0
- package/src/domains/legal/__tests__/setup.ts +82 -0
- package/src/domains/legal/domain/entities/LegalConfig.ts +26 -0
- package/src/domains/legal/domain/services/ContentValidationService.ts +89 -0
- package/src/domains/legal/domain/services/StyleCacheService.ts +97 -0
- package/src/domains/legal/domain/services/UrlHandlerService.ts +128 -0
- package/src/domains/legal/index.ts +8 -0
- package/src/domains/legal/presentation/components/LegalItem.tsx +177 -0
- package/src/domains/legal/presentation/components/LegalLinks.tsx +154 -0
- package/src/domains/legal/presentation/components/LegalSection.tsx +134 -0
- package/src/domains/legal/presentation/screens/LegalScreen.tsx +237 -0
- package/src/domains/legal/presentation/screens/PrivacyPolicyScreen.tsx +214 -0
- package/src/domains/legal/presentation/screens/TermsOfServiceScreen.tsx +214 -0
- package/src/index.ts +19 -0
- package/src/presentation/components/DevSettingsSection.tsx +2 -2
- package/src/presentation/components/SettingItem.tsx +2 -2
- package/src/presentation/components/SettingsErrorBoundary.tsx +2 -2
- package/src/presentation/components/SettingsFooter.tsx +2 -2
- package/src/presentation/components/SettingsSection.tsx +2 -2
- package/src/presentation/navigation/SettingsStackNavigator.tsx +2 -2
- package/src/presentation/screens/SettingsScreen.tsx +2 -2
- package/src/presentation/screens/components/SettingsContent.tsx +2 -2
- package/src/presentation/screens/components/SettingsHeader.tsx +2 -2
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appearance Storage Service
|
|
3
|
+
*
|
|
4
|
+
* Handles persistence of appearance settings using AsyncStorage
|
|
5
|
+
* Single Responsibility: Pure storage operations only
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { storageRepository, unwrap } from "@umituz/react-native-storage";
|
|
9
|
+
import type { ThemeMode } from "@umituz/react-native-design-system";
|
|
10
|
+
import type { AppearanceSettings } from "../../types";
|
|
11
|
+
|
|
12
|
+
const STORAGE_KEYS = {
|
|
13
|
+
APPEARANCE_SETTINGS: "@appearance_settings",
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
const DEFAULT_SETTINGS: AppearanceSettings = {
|
|
17
|
+
themeMode: "dark",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class AppearanceStorage {
|
|
21
|
+
/**
|
|
22
|
+
* Get saved appearance settings
|
|
23
|
+
* Pure storage operation - no business logic
|
|
24
|
+
*/
|
|
25
|
+
static async getSettings(): Promise<AppearanceSettings | null> {
|
|
26
|
+
try {
|
|
27
|
+
const result = await storageRepository.getItem<AppearanceSettings>(
|
|
28
|
+
STORAGE_KEYS.APPEARANCE_SETTINGS,
|
|
29
|
+
DEFAULT_SETTINGS,
|
|
30
|
+
);
|
|
31
|
+
const data = unwrap(result, DEFAULT_SETTINGS);
|
|
32
|
+
return data;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Save appearance settings
|
|
40
|
+
* Pure storage operation - no business logic
|
|
41
|
+
*/
|
|
42
|
+
static async setSettings(settings: AppearanceSettings): Promise<void> {
|
|
43
|
+
const result = await storageRepository.setItem(
|
|
44
|
+
STORAGE_KEYS.APPEARANCE_SETTINGS,
|
|
45
|
+
settings,
|
|
46
|
+
);
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
throw new Error("Failed to save appearance settings");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get saved theme mode
|
|
54
|
+
* Pure storage operation - no business logic
|
|
55
|
+
*/
|
|
56
|
+
static async getThemeMode(): Promise<ThemeMode | null> {
|
|
57
|
+
try {
|
|
58
|
+
const settings = await this.getSettings();
|
|
59
|
+
return settings?.themeMode || null;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Save theme mode
|
|
67
|
+
* Pure storage operation - no business logic
|
|
68
|
+
*/
|
|
69
|
+
static async setThemeMode(themeMode: ThemeMode): Promise<void> {
|
|
70
|
+
const currentSettings = (await this.getSettings()) || {
|
|
71
|
+
themeMode: "dark",
|
|
72
|
+
};
|
|
73
|
+
await this.setSettings({
|
|
74
|
+
...currentSettings,
|
|
75
|
+
themeMode,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get custom theme colors
|
|
81
|
+
* Pure storage operation - no business logic
|
|
82
|
+
*/
|
|
83
|
+
static async getCustomColors() {
|
|
84
|
+
try {
|
|
85
|
+
const settings = await this.getSettings();
|
|
86
|
+
return settings?.customColors || null;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Save custom theme colors
|
|
94
|
+
* Pure storage operation - no business logic
|
|
95
|
+
*/
|
|
96
|
+
static async setCustomColors(
|
|
97
|
+
customColors: AppearanceSettings["customColors"],
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
const currentSettings = (await this.getSettings()) || {
|
|
100
|
+
themeMode: "dark",
|
|
101
|
+
};
|
|
102
|
+
await this.setSettings({
|
|
103
|
+
...currentSettings,
|
|
104
|
+
customColors,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Clear all appearance settings
|
|
110
|
+
* Pure storage operation - no business logic
|
|
111
|
+
*/
|
|
112
|
+
static async clear(): Promise<void> {
|
|
113
|
+
const result = await storageRepository.removeItem(
|
|
114
|
+
STORAGE_KEYS.APPEARANCE_SETTINGS,
|
|
115
|
+
);
|
|
116
|
+
if (!result.success) {
|
|
117
|
+
throw new Error("Failed to clear appearance settings");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appearance Store
|
|
3
|
+
*
|
|
4
|
+
* Zustand store for appearance state management
|
|
5
|
+
* Single Responsibility: Pure state management only
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { create } from "zustand";
|
|
9
|
+
import type { AppearanceSettings, AppearanceState } from "../../types";
|
|
10
|
+
|
|
11
|
+
interface AppearanceStoreActions {
|
|
12
|
+
// Pure state mutations only
|
|
13
|
+
setSettings: (settings: AppearanceSettings) => void;
|
|
14
|
+
setInitialized: (initialized: boolean) => void;
|
|
15
|
+
updateThemeMode: (mode: AppearanceSettings["themeMode"]) => void;
|
|
16
|
+
updateCustomColors: (colors: AppearanceSettings["customColors"]) => void;
|
|
17
|
+
resetState: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type AppearanceStore = AppearanceState & AppearanceStoreActions;
|
|
21
|
+
|
|
22
|
+
const DEFAULT_SETTINGS: AppearanceSettings = {
|
|
23
|
+
themeMode: "dark", // Use dark mode as default
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const useAppearanceStore = create<AppearanceStore>((set, get) => ({
|
|
27
|
+
settings: DEFAULT_SETTINGS,
|
|
28
|
+
isInitialized: false,
|
|
29
|
+
|
|
30
|
+
// Pure state mutations with performance optimizations
|
|
31
|
+
setSettings: (settings: AppearanceSettings) => {
|
|
32
|
+
// Prevent unnecessary updates if settings are the same
|
|
33
|
+
const currentSettings = get().settings;
|
|
34
|
+
if (JSON.stringify(currentSettings) === JSON.stringify(settings)) {
|
|
35
|
+
if (__DEV__) {
|
|
36
|
+
console.log("[AppearanceStore] Skipping settings update - no changes");
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (__DEV__) {
|
|
42
|
+
console.log("[AppearanceStore] Setting appearance settings:", settings);
|
|
43
|
+
}
|
|
44
|
+
set({ settings });
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
setInitialized: (initialized: boolean) => {
|
|
48
|
+
// Prevent unnecessary updates if state is the same
|
|
49
|
+
const currentInitialized = get().isInitialized;
|
|
50
|
+
if (currentInitialized === initialized) {
|
|
51
|
+
if (__DEV__) {
|
|
52
|
+
console.log("[AppearanceStore] Skipping initialized update - no change");
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (__DEV__) {
|
|
58
|
+
console.log("[AppearanceStore] Setting initialized state:", initialized);
|
|
59
|
+
}
|
|
60
|
+
set({ isInitialized: initialized });
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
updateThemeMode: (mode: AppearanceSettings["themeMode"]) => {
|
|
64
|
+
const currentSettings = get().settings;
|
|
65
|
+
|
|
66
|
+
// Prevent unnecessary updates if mode is the same
|
|
67
|
+
if (currentSettings.themeMode === mode) {
|
|
68
|
+
if (__DEV__) {
|
|
69
|
+
console.log("[AppearanceStore] Skipping theme mode update - no change");
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const newSettings: AppearanceSettings = {
|
|
75
|
+
...currentSettings,
|
|
76
|
+
themeMode: mode,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (__DEV__) {
|
|
80
|
+
console.log("[AppearanceStore] Updating theme mode:", mode);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
set({ settings: newSettings });
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
updateCustomColors: (colors: AppearanceSettings["customColors"]) => {
|
|
87
|
+
const currentSettings = get().settings;
|
|
88
|
+
|
|
89
|
+
// Prevent unnecessary updates if colors are the same
|
|
90
|
+
if (JSON.stringify(currentSettings.customColors) === JSON.stringify(colors)) {
|
|
91
|
+
if (__DEV__) {
|
|
92
|
+
console.log("[AppearanceStore] Skipping custom colors update - no changes");
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const newSettings: AppearanceSettings = {
|
|
98
|
+
...currentSettings,
|
|
99
|
+
customColors: colors,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (__DEV__) {
|
|
103
|
+
console.log("[AppearanceStore] Updating custom colors:", colors);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
set({ settings: newSettings });
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
resetState: () => {
|
|
110
|
+
const currentState = get();
|
|
111
|
+
|
|
112
|
+
// Prevent unnecessary reset if already at default
|
|
113
|
+
if (
|
|
114
|
+
currentState.isInitialized === false &&
|
|
115
|
+
JSON.stringify(currentState.settings) === JSON.stringify(DEFAULT_SETTINGS)
|
|
116
|
+
) {
|
|
117
|
+
if (__DEV__) {
|
|
118
|
+
console.log("[AppearanceStore] Skipping reset - already at default state");
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (__DEV__) {
|
|
124
|
+
console.log("[AppearanceStore] Resetting to default state");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
set({
|
|
128
|
+
settings: DEFAULT_SETTINGS,
|
|
129
|
+
isInitialized: false,
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
}));
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appearance Header Component
|
|
3
|
+
* Single Responsibility: Render appearance screen header
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText } from "@umituz/react-native-design-system";
|
|
9
|
+
import type { DesignTokens } from "@umituz/react-native-design-system";
|
|
10
|
+
|
|
11
|
+
export interface AppearanceHeaderProps {
|
|
12
|
+
tokens: DesignTokens;
|
|
13
|
+
title?: string;
|
|
14
|
+
subtitle?: string;
|
|
15
|
+
titleType?: "headlineLarge" | "headlineMedium" | "headlineSmall";
|
|
16
|
+
subtitleType?: "bodyLarge" | "bodyMedium" | "bodySmall";
|
|
17
|
+
titleColor?: "primary" | "secondary" | "tertiary";
|
|
18
|
+
subtitleColor?: "primary" | "secondary" | "tertiary";
|
|
19
|
+
style?: any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const AppearanceHeader: React.FC<AppearanceHeaderProps> = ({
|
|
23
|
+
tokens,
|
|
24
|
+
title,
|
|
25
|
+
subtitle,
|
|
26
|
+
titleType = "headlineLarge",
|
|
27
|
+
subtitleType = "bodyMedium",
|
|
28
|
+
titleColor = "primary",
|
|
29
|
+
subtitleColor = "secondary",
|
|
30
|
+
style,
|
|
31
|
+
}) => {
|
|
32
|
+
// Don't render anything if there's no content
|
|
33
|
+
if (!title && !subtitle) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const styles = getStyles(tokens);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View style={[styles.header, style]}>
|
|
41
|
+
{title ? (
|
|
42
|
+
<AtomicText type={titleType} color={titleColor}>
|
|
43
|
+
{title}
|
|
44
|
+
</AtomicText>
|
|
45
|
+
) : null}
|
|
46
|
+
{subtitle ? (
|
|
47
|
+
<AtomicText
|
|
48
|
+
type={subtitleType}
|
|
49
|
+
color={subtitleColor}
|
|
50
|
+
style={styles.headerSubtitle}
|
|
51
|
+
>
|
|
52
|
+
{subtitle}
|
|
53
|
+
</AtomicText>
|
|
54
|
+
) : null}
|
|
55
|
+
</View>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getStyles = (tokens: DesignTokens) =>
|
|
60
|
+
StyleSheet.create({
|
|
61
|
+
header: {
|
|
62
|
+
marginBottom: tokens.spacing.lg,
|
|
63
|
+
},
|
|
64
|
+
headerSubtitle: {
|
|
65
|
+
marginTop: tokens.spacing.sm,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appearance Preview Component
|
|
3
|
+
* Single Responsibility: Render color preview section
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText } from "@umituz/react-native-design-system";
|
|
9
|
+
import type { DesignTokens } from "@umituz/react-native-design-system";
|
|
10
|
+
import type { CustomThemeColors } from "../../types";
|
|
11
|
+
|
|
12
|
+
export interface PreviewColorItem {
|
|
13
|
+
key: keyof CustomThemeColors;
|
|
14
|
+
label: string;
|
|
15
|
+
fallbackColor: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AppearancePreviewProps {
|
|
19
|
+
tokens: DesignTokens;
|
|
20
|
+
localCustomColors: CustomThemeColors;
|
|
21
|
+
title?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
previewColors?: PreviewColorItem[];
|
|
24
|
+
showPreview?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_PREVIEW_COLORS: PreviewColorItem[] = [
|
|
28
|
+
{
|
|
29
|
+
key: "primary",
|
|
30
|
+
label: "Primary",
|
|
31
|
+
fallbackColor: "#007AFF",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "secondary",
|
|
35
|
+
label: "Secondary",
|
|
36
|
+
fallbackColor: "#8E8E93",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "accent",
|
|
40
|
+
label: "Accent",
|
|
41
|
+
fallbackColor: "#FF6B6B",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "buttonPrimary",
|
|
45
|
+
label: "Button",
|
|
46
|
+
fallbackColor: "#007AFF",
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const AppearancePreview: React.FC<AppearancePreviewProps> = ({
|
|
51
|
+
tokens,
|
|
52
|
+
localCustomColors,
|
|
53
|
+
title,
|
|
54
|
+
description,
|
|
55
|
+
previewColors = DEFAULT_PREVIEW_COLORS,
|
|
56
|
+
showPreview = true,
|
|
57
|
+
}) => {
|
|
58
|
+
const styles = getStyles(tokens);
|
|
59
|
+
|
|
60
|
+
const colors = previewColors.map((item) => ({
|
|
61
|
+
label: item.label,
|
|
62
|
+
color: localCustomColors[item.key] || item.fallbackColor,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
if (!showPreview) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<View style={styles.previewSection}>
|
|
71
|
+
<AtomicText
|
|
72
|
+
type="titleMedium"
|
|
73
|
+
color="primary"
|
|
74
|
+
style={styles.sectionTitle}
|
|
75
|
+
>
|
|
76
|
+
{title}
|
|
77
|
+
</AtomicText>
|
|
78
|
+
<AtomicText
|
|
79
|
+
type="bodySmall"
|
|
80
|
+
color="secondary"
|
|
81
|
+
style={styles.sectionDescription}
|
|
82
|
+
>
|
|
83
|
+
{description}
|
|
84
|
+
</AtomicText>
|
|
85
|
+
<View style={styles.previewContainer}>
|
|
86
|
+
<View style={styles.previewColorRow}>
|
|
87
|
+
{colors.map((item, index) => (
|
|
88
|
+
<View key={index} style={styles.previewColorItem}>
|
|
89
|
+
<View
|
|
90
|
+
style={[
|
|
91
|
+
styles.previewColorCircle,
|
|
92
|
+
{ backgroundColor: item.color },
|
|
93
|
+
]}
|
|
94
|
+
/>
|
|
95
|
+
<AtomicText type="bodySmall" color="secondary">
|
|
96
|
+
{item.label}
|
|
97
|
+
</AtomicText>
|
|
98
|
+
</View>
|
|
99
|
+
))}
|
|
100
|
+
</View>
|
|
101
|
+
</View>
|
|
102
|
+
</View>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const getStyles = (tokens: DesignTokens) =>
|
|
107
|
+
StyleSheet.create({
|
|
108
|
+
previewSection: {
|
|
109
|
+
marginBottom: tokens.spacing.lg,
|
|
110
|
+
},
|
|
111
|
+
sectionTitle: {
|
|
112
|
+
marginBottom: tokens.spacing.xs,
|
|
113
|
+
},
|
|
114
|
+
sectionDescription: {
|
|
115
|
+
marginBottom: tokens.spacing.md,
|
|
116
|
+
},
|
|
117
|
+
previewContainer: {
|
|
118
|
+
backgroundColor: tokens.colors.surface,
|
|
119
|
+
borderRadius: 12,
|
|
120
|
+
padding: tokens.spacing.md,
|
|
121
|
+
borderLeftWidth: 4,
|
|
122
|
+
borderLeftColor: tokens.colors.primary,
|
|
123
|
+
},
|
|
124
|
+
previewColorRow: {
|
|
125
|
+
flexDirection: "row",
|
|
126
|
+
flexWrap: "wrap",
|
|
127
|
+
gap: tokens.spacing.md,
|
|
128
|
+
marginTop: tokens.spacing.sm,
|
|
129
|
+
},
|
|
130
|
+
previewColorItem: {
|
|
131
|
+
alignItems: "center",
|
|
132
|
+
},
|
|
133
|
+
previewColorCircle: {
|
|
134
|
+
width: 32,
|
|
135
|
+
height: 32,
|
|
136
|
+
borderRadius: 16,
|
|
137
|
+
marginBottom: tokens.spacing.xs,
|
|
138
|
+
borderWidth: 1,
|
|
139
|
+
borderColor: tokens.colors.border,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Pressable, StyleSheet, ViewStyle } from 'react-native';
|
|
3
|
+
import { useNavigation } from '@react-navigation/native';
|
|
4
|
+
import { useResponsiveDesignTokens, AtomicIcon, AtomicText } from '@umituz/react-native-design-system';
|
|
5
|
+
import { AppearanceSectionConfig } from '../../types';
|
|
6
|
+
|
|
7
|
+
export interface AppearanceSectionProps {
|
|
8
|
+
config?: AppearanceSectionConfig;
|
|
9
|
+
onPress?: () => void;
|
|
10
|
+
containerStyle?: ViewStyle;
|
|
11
|
+
sectionTitle?: string;
|
|
12
|
+
/** Optional explicit title override */
|
|
13
|
+
title?: string;
|
|
14
|
+
/** Optional explicit description override */
|
|
15
|
+
description?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const AppearanceSection: React.FC<AppearanceSectionProps> = ({
|
|
19
|
+
config,
|
|
20
|
+
onPress,
|
|
21
|
+
containerStyle,
|
|
22
|
+
sectionTitle,
|
|
23
|
+
title: titleProp,
|
|
24
|
+
description: descriptionProp,
|
|
25
|
+
}) => {
|
|
26
|
+
const navigation = useNavigation();
|
|
27
|
+
const tokens = useResponsiveDesignTokens();
|
|
28
|
+
const colors = tokens.colors;
|
|
29
|
+
|
|
30
|
+
const route = config?.route || config?.defaultRoute || 'Appearance';
|
|
31
|
+
// Use props first, then config, then strict empty string to avoid hardcoded English
|
|
32
|
+
const title = titleProp || config?.title;
|
|
33
|
+
const description = descriptionProp || config?.description;
|
|
34
|
+
|
|
35
|
+
// Only display section title if provided
|
|
36
|
+
const displaySectionTitle = sectionTitle || title;
|
|
37
|
+
|
|
38
|
+
const handlePress = () => {
|
|
39
|
+
if (onPress) {
|
|
40
|
+
onPress();
|
|
41
|
+
} else {
|
|
42
|
+
navigation.navigate(route as never);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (!title) return null;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View style={[styles.sectionContainer, { backgroundColor: colors.surface }, containerStyle]}>
|
|
50
|
+
{!!displaySectionTitle && (
|
|
51
|
+
<View style={styles.headerContainer}>
|
|
52
|
+
<AtomicText
|
|
53
|
+
type="titleMedium"
|
|
54
|
+
color="primary"
|
|
55
|
+
>
|
|
56
|
+
{displaySectionTitle}
|
|
57
|
+
</AtomicText>
|
|
58
|
+
</View>
|
|
59
|
+
)}
|
|
60
|
+
<Pressable
|
|
61
|
+
style={({ pressed }) => [
|
|
62
|
+
styles.itemContainer,
|
|
63
|
+
{
|
|
64
|
+
backgroundColor: pressed ? `${colors.primary}08` : 'transparent',
|
|
65
|
+
},
|
|
66
|
+
]}
|
|
67
|
+
onPress={handlePress}
|
|
68
|
+
>
|
|
69
|
+
<View style={styles.content}>
|
|
70
|
+
<View
|
|
71
|
+
style={[
|
|
72
|
+
styles.iconContainer,
|
|
73
|
+
{ backgroundColor: `${colors.primary}15` },
|
|
74
|
+
]}
|
|
75
|
+
>
|
|
76
|
+
<AtomicIcon name="droplet" size="lg" color="primary" />
|
|
77
|
+
</View>
|
|
78
|
+
<View style={styles.textContainer}>
|
|
79
|
+
<AtomicText
|
|
80
|
+
type="bodyLarge"
|
|
81
|
+
color="primary"
|
|
82
|
+
numberOfLines={1}
|
|
83
|
+
style={{ marginBottom: 4 }}
|
|
84
|
+
>
|
|
85
|
+
{title}
|
|
86
|
+
</AtomicText>
|
|
87
|
+
{!!description && (
|
|
88
|
+
<AtomicText
|
|
89
|
+
type="bodyMedium"
|
|
90
|
+
color="secondary"
|
|
91
|
+
numberOfLines={2}
|
|
92
|
+
>
|
|
93
|
+
{description}
|
|
94
|
+
</AtomicText>
|
|
95
|
+
)}
|
|
96
|
+
</View>
|
|
97
|
+
<AtomicIcon name="chevron-right" size="md" color="secondary" />
|
|
98
|
+
</View>
|
|
99
|
+
</Pressable>
|
|
100
|
+
</View>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const styles = StyleSheet.create({
|
|
105
|
+
sectionContainer: {
|
|
106
|
+
marginBottom: 16,
|
|
107
|
+
borderRadius: 12,
|
|
108
|
+
overflow: 'hidden',
|
|
109
|
+
},
|
|
110
|
+
headerContainer: {
|
|
111
|
+
paddingHorizontal: 16,
|
|
112
|
+
paddingTop: 16,
|
|
113
|
+
paddingBottom: 8,
|
|
114
|
+
},
|
|
115
|
+
itemContainer: {
|
|
116
|
+
flexDirection: 'row',
|
|
117
|
+
alignItems: 'center',
|
|
118
|
+
paddingHorizontal: 16,
|
|
119
|
+
paddingVertical: 16,
|
|
120
|
+
minHeight: 72,
|
|
121
|
+
},
|
|
122
|
+
content: {
|
|
123
|
+
flex: 1,
|
|
124
|
+
flexDirection: 'row',
|
|
125
|
+
alignItems: 'center',
|
|
126
|
+
},
|
|
127
|
+
iconContainer: {
|
|
128
|
+
width: 48,
|
|
129
|
+
height: 48,
|
|
130
|
+
borderRadius: 12,
|
|
131
|
+
justifyContent: 'center',
|
|
132
|
+
alignItems: 'center',
|
|
133
|
+
marginRight: 16,
|
|
134
|
+
},
|
|
135
|
+
textContainer: {
|
|
136
|
+
flex: 1,
|
|
137
|
+
marginRight: 8,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color Picker Component
|
|
3
|
+
*
|
|
4
|
+
* Simple color picker for theme customization
|
|
5
|
+
* Single Responsibility: Render color selection UI
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo, useCallback } from "react";
|
|
9
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
10
|
+
import {
|
|
11
|
+
AtomicIcon,
|
|
12
|
+
AtomicText,
|
|
13
|
+
useResponsiveDesignTokens,
|
|
14
|
+
} from "@umituz/react-native-design-system";
|
|
15
|
+
|
|
16
|
+
interface ColorPickerProps {
|
|
17
|
+
label: string;
|
|
18
|
+
value: string;
|
|
19
|
+
onValueChange: (color: string) => void;
|
|
20
|
+
colors: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ColorPicker: React.FC<ColorPickerProps> = ({
|
|
24
|
+
label,
|
|
25
|
+
value,
|
|
26
|
+
onValueChange,
|
|
27
|
+
colors,
|
|
28
|
+
}) => {
|
|
29
|
+
const tokens = useResponsiveDesignTokens();
|
|
30
|
+
|
|
31
|
+
// Memoize styles to prevent unnecessary re-creation
|
|
32
|
+
const styles = useMemo(() => getStyles(tokens), [tokens]);
|
|
33
|
+
|
|
34
|
+
// Memoize colors array to prevent unnecessary re-renders
|
|
35
|
+
const colorsMemo = useMemo(() => colors, [colors]);
|
|
36
|
+
|
|
37
|
+
// Stable callback for color change to prevent infinite re-renders
|
|
38
|
+
const handleColorChange = useCallback((color: string) => {
|
|
39
|
+
try {
|
|
40
|
+
// Prevent unnecessary updates if color hasn't changed
|
|
41
|
+
if (value === color) return;
|
|
42
|
+
|
|
43
|
+
onValueChange(color);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (__DEV__) {
|
|
46
|
+
console.error("[ColorPicker] Failed to change color:", error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}, [value, onValueChange]);
|
|
50
|
+
|
|
51
|
+
// Memoize color options to prevent unnecessary re-renders
|
|
52
|
+
const colorOptions = useMemo(() => {
|
|
53
|
+
return colorsMemo.map((color) => {
|
|
54
|
+
const isSelected = value === color;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<TouchableOpacity
|
|
58
|
+
key={color}
|
|
59
|
+
style={[
|
|
60
|
+
styles.colorOption,
|
|
61
|
+
{ backgroundColor: color },
|
|
62
|
+
isSelected && styles.selectedColor,
|
|
63
|
+
]}
|
|
64
|
+
onPress={() => handleColorChange(color)}
|
|
65
|
+
activeOpacity={0.8} // Performance optimization
|
|
66
|
+
>
|
|
67
|
+
{isSelected && (
|
|
68
|
+
<AtomicIcon name="checkmark" size="sm" customColor="#FFFFFF" />
|
|
69
|
+
)}
|
|
70
|
+
</TouchableOpacity>
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
}, [colorsMemo, value, handleColorChange, styles]);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<View style={styles.container}>
|
|
77
|
+
<AtomicText type="bodyMedium" color="primary" style={styles.label}>
|
|
78
|
+
{label}
|
|
79
|
+
</AtomicText>
|
|
80
|
+
<View style={styles.colorRow}>
|
|
81
|
+
{colorOptions}
|
|
82
|
+
</View>
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getStyles = (tokens: ReturnType<typeof useResponsiveDesignTokens>) =>
|
|
88
|
+
StyleSheet.create({
|
|
89
|
+
container: {
|
|
90
|
+
marginBottom: 16,
|
|
91
|
+
},
|
|
92
|
+
label: {
|
|
93
|
+
marginBottom: 8,
|
|
94
|
+
},
|
|
95
|
+
colorRow: {
|
|
96
|
+
flexDirection: "row",
|
|
97
|
+
flexWrap: "wrap",
|
|
98
|
+
gap: 12,
|
|
99
|
+
},
|
|
100
|
+
colorOption: {
|
|
101
|
+
width: 40,
|
|
102
|
+
height: 40,
|
|
103
|
+
borderRadius: 20,
|
|
104
|
+
borderWidth: 2,
|
|
105
|
+
borderColor: tokens.colors.border,
|
|
106
|
+
justifyContent: "center",
|
|
107
|
+
alignItems: "center",
|
|
108
|
+
},
|
|
109
|
+
selectedColor: {
|
|
110
|
+
borderColor: tokens.colors.primary,
|
|
111
|
+
borderWidth: 3,
|
|
112
|
+
},
|
|
113
|
+
});
|