@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,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Colors Section Component
|
|
3
|
+
* Single Responsibility: Render custom color picker section
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo, useCallback } from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText, AtomicButton } from "@umituz/react-native-design-system";
|
|
9
|
+
import { ColorPicker } from "./ColorPicker";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_PRIMARY_COLORS,
|
|
12
|
+
DEFAULT_SECONDARY_COLORS,
|
|
13
|
+
DEFAULT_ACCENT_COLORS,
|
|
14
|
+
} from "../../data/colorPalettes";
|
|
15
|
+
import type { DesignTokens } from "@umituz/react-native-design-system";
|
|
16
|
+
import type { CustomThemeColors } from "../../types";
|
|
17
|
+
|
|
18
|
+
export interface ColorFieldConfig {
|
|
19
|
+
key: keyof CustomThemeColors;
|
|
20
|
+
label: string;
|
|
21
|
+
defaultColor: string;
|
|
22
|
+
colorPalette: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CustomColorsSectionProps {
|
|
26
|
+
tokens: DesignTokens;
|
|
27
|
+
localCustomColors: CustomThemeColors;
|
|
28
|
+
onColorChange: (key: keyof CustomThemeColors, color: string) => void;
|
|
29
|
+
onResetColors: () => void;
|
|
30
|
+
title?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
resetButtonText?: string;
|
|
33
|
+
colorFields?: ColorFieldConfig[];
|
|
34
|
+
showResetButton?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DEFAULT_COLOR_FIELDS: ColorFieldConfig[] = [
|
|
38
|
+
{
|
|
39
|
+
key: "primary",
|
|
40
|
+
label: "Primary Color",
|
|
41
|
+
defaultColor: "#007AFF",
|
|
42
|
+
colorPalette: DEFAULT_PRIMARY_COLORS.colors,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "secondary",
|
|
46
|
+
label: "Secondary Color",
|
|
47
|
+
defaultColor: "#8E8E93",
|
|
48
|
+
colorPalette: DEFAULT_SECONDARY_COLORS.colors,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: "accent",
|
|
52
|
+
label: "Accent Color",
|
|
53
|
+
defaultColor: "#FF6B6B",
|
|
54
|
+
colorPalette: DEFAULT_ACCENT_COLORS.colors,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: "buttonPrimary",
|
|
58
|
+
label: "Button Primary",
|
|
59
|
+
defaultColor: "#007AFF",
|
|
60
|
+
colorPalette: DEFAULT_PRIMARY_COLORS.colors,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: "buttonSecondary",
|
|
64
|
+
label: "Button Secondary",
|
|
65
|
+
defaultColor: "#8E8E93",
|
|
66
|
+
colorPalette: DEFAULT_SECONDARY_COLORS.colors,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
export const CustomColorsSection: React.FC<CustomColorsSectionProps> = ({
|
|
71
|
+
tokens,
|
|
72
|
+
localCustomColors,
|
|
73
|
+
onColorChange,
|
|
74
|
+
onResetColors,
|
|
75
|
+
title,
|
|
76
|
+
description,
|
|
77
|
+
resetButtonText,
|
|
78
|
+
colorFields = DEFAULT_COLOR_FIELDS,
|
|
79
|
+
showResetButton = true,
|
|
80
|
+
}) => {
|
|
81
|
+
// Memoize styles to prevent unnecessary re-creation
|
|
82
|
+
const styles = useMemo(() => getStyles(tokens), [tokens]);
|
|
83
|
+
|
|
84
|
+
// Memoize hasCustomColors check to prevent unnecessary re-renders
|
|
85
|
+
const hasCustomColors = useMemo(() =>
|
|
86
|
+
Object.keys(localCustomColors).length > 0,
|
|
87
|
+
[localCustomColors]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Memoize color fields to prevent unnecessary re-renders
|
|
91
|
+
const colorFieldsMemo = useMemo(() => colorFields, [colorFields]);
|
|
92
|
+
|
|
93
|
+
// Stable callback for color change to prevent infinite re-renders
|
|
94
|
+
const handleColorChange = useCallback((key: keyof CustomThemeColors, color: string) => {
|
|
95
|
+
try {
|
|
96
|
+
onColorChange(key, color);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (__DEV__) {
|
|
99
|
+
console.error("[CustomColorsSection] Failed to change color:", error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}, [onColorChange]);
|
|
103
|
+
|
|
104
|
+
// Stable callback for reset to prevent infinite re-renders
|
|
105
|
+
const handleResetColors = useCallback(() => {
|
|
106
|
+
try {
|
|
107
|
+
onResetColors();
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (__DEV__) {
|
|
110
|
+
console.error("[CustomColorsSection] Failed to reset colors:", error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}, [onResetColors]);
|
|
114
|
+
|
|
115
|
+
// Memoize color pickers to prevent unnecessary re-renders
|
|
116
|
+
const colorPickers = useMemo(() => {
|
|
117
|
+
return colorFieldsMemo.map((field) => (
|
|
118
|
+
<ColorPicker
|
|
119
|
+
key={field.key}
|
|
120
|
+
label={field.label}
|
|
121
|
+
value={localCustomColors[field.key] || field.defaultColor}
|
|
122
|
+
onValueChange={(color) => handleColorChange(field.key, color)}
|
|
123
|
+
colors={field.colorPalette}
|
|
124
|
+
/>
|
|
125
|
+
));
|
|
126
|
+
}, [colorFieldsMemo, localCustomColors, handleColorChange]);
|
|
127
|
+
|
|
128
|
+
// Memoize reset button to prevent unnecessary re-renders
|
|
129
|
+
const resetButton = useMemo(() => {
|
|
130
|
+
if (!showResetButton || !hasCustomColors) return null;
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<AtomicButton variant="outline" size="sm" onPress={handleResetColors}>
|
|
134
|
+
{resetButtonText}
|
|
135
|
+
</AtomicButton>
|
|
136
|
+
);
|
|
137
|
+
}, [showResetButton, hasCustomColors, resetButtonText, handleResetColors]);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<View style={styles.section}>
|
|
141
|
+
<View style={styles.sectionHeader}>
|
|
142
|
+
<View style={styles.titleContainer}>
|
|
143
|
+
<AtomicText
|
|
144
|
+
type="titleMedium"
|
|
145
|
+
color="primary"
|
|
146
|
+
style={styles.sectionTitle}
|
|
147
|
+
>
|
|
148
|
+
{title}
|
|
149
|
+
</AtomicText>
|
|
150
|
+
<AtomicText
|
|
151
|
+
type="bodySmall"
|
|
152
|
+
color="secondary"
|
|
153
|
+
style={styles.sectionDescription}
|
|
154
|
+
>
|
|
155
|
+
{description}
|
|
156
|
+
</AtomicText>
|
|
157
|
+
</View>
|
|
158
|
+
{resetButton}
|
|
159
|
+
</View>
|
|
160
|
+
|
|
161
|
+
{colorPickers}
|
|
162
|
+
</View>
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const getStyles = (tokens: DesignTokens) =>
|
|
167
|
+
StyleSheet.create({
|
|
168
|
+
section: {
|
|
169
|
+
marginBottom: tokens.spacing.xl,
|
|
170
|
+
},
|
|
171
|
+
sectionHeader: {
|
|
172
|
+
flexDirection: "row",
|
|
173
|
+
justifyContent: "space-between",
|
|
174
|
+
alignItems: "flex-start",
|
|
175
|
+
marginBottom: tokens.spacing.md,
|
|
176
|
+
},
|
|
177
|
+
titleContainer: {
|
|
178
|
+
flex: 1,
|
|
179
|
+
},
|
|
180
|
+
sectionTitle: {
|
|
181
|
+
marginBottom: tokens.spacing.xs,
|
|
182
|
+
},
|
|
183
|
+
sectionDescription: {
|
|
184
|
+
marginBottom: tokens.spacing.md,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Mode Section Component
|
|
3
|
+
* Single Responsibility: Render theme mode selection section
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo, useCallback } from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText } from "@umituz/react-native-design-system";
|
|
9
|
+
import { ThemeOption } from "./ThemeOption";
|
|
10
|
+
import type { DesignTokens } from "@umituz/react-native-design-system";
|
|
11
|
+
import type { ThemeMode } from "../../types";
|
|
12
|
+
|
|
13
|
+
export interface ThemeOptionConfig {
|
|
14
|
+
mode: ThemeMode;
|
|
15
|
+
title: string;
|
|
16
|
+
subtitle?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
features: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ThemeModeSectionProps {
|
|
22
|
+
tokens: DesignTokens;
|
|
23
|
+
themeMode: ThemeMode;
|
|
24
|
+
onThemeSelect: (mode: ThemeMode) => void;
|
|
25
|
+
title?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
themes?: ThemeOptionConfig[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const ThemeModeSection: React.FC<ThemeModeSectionProps> = ({
|
|
31
|
+
tokens,
|
|
32
|
+
themeMode,
|
|
33
|
+
onThemeSelect,
|
|
34
|
+
title,
|
|
35
|
+
description,
|
|
36
|
+
themes,
|
|
37
|
+
}) => {
|
|
38
|
+
// Memoize styles to prevent unnecessary re-creation
|
|
39
|
+
const styles = useMemo(() => getStyles(tokens), [tokens]);
|
|
40
|
+
|
|
41
|
+
// Stable callback for theme selection to prevent infinite re-renders
|
|
42
|
+
const handleThemeSelect = useCallback((mode: ThemeMode) => {
|
|
43
|
+
try {
|
|
44
|
+
onThemeSelect(mode);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (__DEV__) {
|
|
47
|
+
console.error("[ThemeModeSection] Failed to select theme:", error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}, [onThemeSelect]);
|
|
51
|
+
|
|
52
|
+
// Memoize theme options
|
|
53
|
+
const themeOptions = useMemo(() => {
|
|
54
|
+
// If no themes provided, we shouldn't render anything or maybe we assume the parent MUST provide them.
|
|
55
|
+
// To be safe and "Package Driven", we require the consumer to provide the content.
|
|
56
|
+
if (!themes || themes.length === 0) return null;
|
|
57
|
+
|
|
58
|
+
return themes.map((theme) => (
|
|
59
|
+
<ThemeOption
|
|
60
|
+
key={theme.mode}
|
|
61
|
+
mode={theme.mode}
|
|
62
|
+
title={theme.title}
|
|
63
|
+
subtitle={theme.subtitle || ""}
|
|
64
|
+
description={theme.description || ""}
|
|
65
|
+
features={theme.features}
|
|
66
|
+
isSelected={themeMode === theme.mode}
|
|
67
|
+
onSelect={() => handleThemeSelect(theme.mode)}
|
|
68
|
+
/>
|
|
69
|
+
));
|
|
70
|
+
}, [themes, themeMode, handleThemeSelect]);
|
|
71
|
+
|
|
72
|
+
if (!themeOptions) return null;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<View style={styles.section}>
|
|
76
|
+
{!!title && (
|
|
77
|
+
<AtomicText
|
|
78
|
+
type="titleMedium"
|
|
79
|
+
color="primary"
|
|
80
|
+
style={styles.sectionTitle}
|
|
81
|
+
>
|
|
82
|
+
{title}
|
|
83
|
+
</AtomicText>
|
|
84
|
+
)}
|
|
85
|
+
{!!description && (
|
|
86
|
+
<AtomicText
|
|
87
|
+
type="bodyMedium"
|
|
88
|
+
color="secondary"
|
|
89
|
+
style={styles.sectionDescription}
|
|
90
|
+
>
|
|
91
|
+
{description}
|
|
92
|
+
</AtomicText>
|
|
93
|
+
)}
|
|
94
|
+
{themeOptions}
|
|
95
|
+
</View>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const getStyles = (tokens: DesignTokens) =>
|
|
100
|
+
StyleSheet.create({
|
|
101
|
+
section: {
|
|
102
|
+
marginBottom: tokens.spacing.xl,
|
|
103
|
+
},
|
|
104
|
+
sectionTitle: {
|
|
105
|
+
marginBottom: tokens.spacing.xs,
|
|
106
|
+
},
|
|
107
|
+
sectionDescription: {
|
|
108
|
+
marginBottom: tokens.spacing.md,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Option Component
|
|
3
|
+
*
|
|
4
|
+
* Theme mode selection option (Light/Dark)
|
|
5
|
+
* Single Responsibility: Render theme option UI
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
10
|
+
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system";
|
|
11
|
+
import { useResponsiveDesignTokens } from "@umituz/react-native-design-system";
|
|
12
|
+
import type { ThemeMode } from "../../types";
|
|
13
|
+
|
|
14
|
+
interface ThemeOptionProps {
|
|
15
|
+
mode: ThemeMode;
|
|
16
|
+
title: string;
|
|
17
|
+
subtitle: string;
|
|
18
|
+
description: string;
|
|
19
|
+
features: string[];
|
|
20
|
+
isSelected: boolean;
|
|
21
|
+
onSelect: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ThemeOption: React.FC<ThemeOptionProps> = ({
|
|
25
|
+
mode,
|
|
26
|
+
title,
|
|
27
|
+
subtitle,
|
|
28
|
+
description,
|
|
29
|
+
features,
|
|
30
|
+
isSelected,
|
|
31
|
+
onSelect,
|
|
32
|
+
}) => {
|
|
33
|
+
const tokens = useResponsiveDesignTokens();
|
|
34
|
+
const styles = getStyles(tokens);
|
|
35
|
+
const iconName = mode === "dark" ? "moon-outline" : mode === "light" ? "sunny-outline" : "desktop-outline";
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<TouchableOpacity
|
|
39
|
+
style={[styles.container, isSelected && styles.selectedContainer]}
|
|
40
|
+
onPress={onSelect}
|
|
41
|
+
>
|
|
42
|
+
<View style={styles.header}>
|
|
43
|
+
<View style={styles.iconContainer}>
|
|
44
|
+
<AtomicIcon name={iconName} customSize={24} customColor={tokens.colors.primary} />
|
|
45
|
+
</View>
|
|
46
|
+
<View style={styles.textContainer}>
|
|
47
|
+
<AtomicText type="titleLarge" color="primary">
|
|
48
|
+
{title}
|
|
49
|
+
</AtomicText>
|
|
50
|
+
<AtomicText type="bodyMedium" color="secondary">
|
|
51
|
+
{subtitle}
|
|
52
|
+
</AtomicText>
|
|
53
|
+
</View>
|
|
54
|
+
<AtomicIcon
|
|
55
|
+
name={isSelected ? "checkmark-circle-outline" : "ellipse-outline"}
|
|
56
|
+
customSize={24}
|
|
57
|
+
customColor={isSelected ? tokens.colors.primary : tokens.colors.secondary}
|
|
58
|
+
/>
|
|
59
|
+
</View>
|
|
60
|
+
|
|
61
|
+
<AtomicText
|
|
62
|
+
type="bodyMedium"
|
|
63
|
+
color="secondary"
|
|
64
|
+
style={styles.description}
|
|
65
|
+
>
|
|
66
|
+
{description}
|
|
67
|
+
</AtomicText>
|
|
68
|
+
|
|
69
|
+
<View style={styles.featuresContainer}>
|
|
70
|
+
<AtomicText
|
|
71
|
+
type="labelLarge"
|
|
72
|
+
color="primary"
|
|
73
|
+
style={styles.featuresTitle}
|
|
74
|
+
>
|
|
75
|
+
Features
|
|
76
|
+
</AtomicText>
|
|
77
|
+
{features.map((feature, index) => (
|
|
78
|
+
<AtomicText
|
|
79
|
+
key={index}
|
|
80
|
+
type="bodySmall"
|
|
81
|
+
color="secondary"
|
|
82
|
+
style={styles.feature}
|
|
83
|
+
>
|
|
84
|
+
• {feature}
|
|
85
|
+
</AtomicText>
|
|
86
|
+
))}
|
|
87
|
+
</View>
|
|
88
|
+
</TouchableOpacity>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const getStyles = (tokens: ReturnType<typeof useResponsiveDesignTokens>) =>
|
|
93
|
+
StyleSheet.create({
|
|
94
|
+
container: {
|
|
95
|
+
backgroundColor: tokens.colors.surface,
|
|
96
|
+
borderRadius: 12,
|
|
97
|
+
padding: 16,
|
|
98
|
+
marginBottom: 16,
|
|
99
|
+
borderWidth: 2,
|
|
100
|
+
borderColor: tokens.colors.border,
|
|
101
|
+
},
|
|
102
|
+
selectedContainer: {
|
|
103
|
+
borderColor: tokens.colors.primary,
|
|
104
|
+
},
|
|
105
|
+
header: {
|
|
106
|
+
flexDirection: "row",
|
|
107
|
+
alignItems: "center",
|
|
108
|
+
marginBottom: 12,
|
|
109
|
+
},
|
|
110
|
+
iconContainer: {
|
|
111
|
+
width: 48,
|
|
112
|
+
height: 48,
|
|
113
|
+
borderRadius: 24,
|
|
114
|
+
backgroundColor: `${tokens.colors.primary}15`,
|
|
115
|
+
justifyContent: "center",
|
|
116
|
+
alignItems: "center",
|
|
117
|
+
marginRight: 12,
|
|
118
|
+
},
|
|
119
|
+
textContainer: {
|
|
120
|
+
flex: 1,
|
|
121
|
+
},
|
|
122
|
+
description: {
|
|
123
|
+
marginBottom: 12,
|
|
124
|
+
lineHeight: 20,
|
|
125
|
+
},
|
|
126
|
+
featuresContainer: {
|
|
127
|
+
backgroundColor: `${tokens.colors.primary}08`,
|
|
128
|
+
borderRadius: 8,
|
|
129
|
+
padding: 12,
|
|
130
|
+
},
|
|
131
|
+
featuresTitle: {
|
|
132
|
+
marginBottom: 8,
|
|
133
|
+
},
|
|
134
|
+
feature: {
|
|
135
|
+
marginBottom: 4,
|
|
136
|
+
lineHeight: 18,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ThemeOption } from "./ThemeOption";
|
|
2
|
+
export { ColorPicker } from "./ColorPicker";
|
|
3
|
+
export { AppearanceHeader } from "./AppearanceHeader";
|
|
4
|
+
export { ThemeModeSection, type ThemeOptionConfig } from "./ThemeModeSection";
|
|
5
|
+
export { CustomColorsSection, type ColorFieldConfig } from "./CustomColorsSection";
|
|
6
|
+
export { AppearancePreview, type PreviewColorItem } from "./AppearancePreview";
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appearance Screen
|
|
3
|
+
*
|
|
4
|
+
* Screen for managing appearance settings including theme mode and custom colors
|
|
5
|
+
* Single Responsibility: Presentation orchestration only
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo, useCallback } from "react";
|
|
9
|
+
import { ScrollView, StyleSheet, View } from "react-native";
|
|
10
|
+
import { useResponsiveDesignTokens } from "@umituz/react-native-design-system";
|
|
11
|
+
import { useAppearance, useAppearanceActions } from "../../hooks";
|
|
12
|
+
import {
|
|
13
|
+
AppearanceHeader,
|
|
14
|
+
ThemeModeSection,
|
|
15
|
+
CustomColorsSection,
|
|
16
|
+
AppearancePreview,
|
|
17
|
+
} from "../components";
|
|
18
|
+
|
|
19
|
+
import type { AppearanceTexts } from "../../types";
|
|
20
|
+
|
|
21
|
+
export interface AppearanceScreenProps {
|
|
22
|
+
/** Texts for localization */
|
|
23
|
+
texts?: AppearanceTexts;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Custom header component to override default header
|
|
27
|
+
*/
|
|
28
|
+
headerComponent?: React.ReactNode;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Show/hide theme mode section
|
|
32
|
+
*/
|
|
33
|
+
showThemeSection?: boolean;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Show/hide custom colors section
|
|
37
|
+
*/
|
|
38
|
+
showColorsSection?: boolean;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Show/hide preview section
|
|
42
|
+
*/
|
|
43
|
+
showPreviewSection?: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Custom container style
|
|
47
|
+
*/
|
|
48
|
+
containerStyle?: any;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Custom content container style
|
|
52
|
+
*/
|
|
53
|
+
contentContainerStyle?: any;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const AppearanceScreen: React.FC<AppearanceScreenProps> = ({
|
|
57
|
+
texts,
|
|
58
|
+
headerComponent,
|
|
59
|
+
showThemeSection = true,
|
|
60
|
+
showColorsSection = true,
|
|
61
|
+
showPreviewSection = true,
|
|
62
|
+
containerStyle,
|
|
63
|
+
contentContainerStyle,
|
|
64
|
+
}) => {
|
|
65
|
+
const tokens = useResponsiveDesignTokens();
|
|
66
|
+
const { themeMode } = useAppearance();
|
|
67
|
+
const {
|
|
68
|
+
localCustomColors,
|
|
69
|
+
handleThemeSelect,
|
|
70
|
+
handleColorChange,
|
|
71
|
+
handleResetColors,
|
|
72
|
+
} = useAppearanceActions();
|
|
73
|
+
|
|
74
|
+
// Memoize styles to prevent unnecessary re-creation
|
|
75
|
+
const styles = useMemo(() => getStyles(tokens), [tokens]);
|
|
76
|
+
|
|
77
|
+
// Memoize header to prevent unnecessary re-renders
|
|
78
|
+
const headerComponentMemo = useMemo(() => {
|
|
79
|
+
return (
|
|
80
|
+
headerComponent || (
|
|
81
|
+
<AppearanceHeader
|
|
82
|
+
tokens={tokens}
|
|
83
|
+
title={texts?.title}
|
|
84
|
+
subtitle={texts?.subtitle}
|
|
85
|
+
/>
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
}, [headerComponent, tokens, texts?.title, texts?.subtitle]);
|
|
89
|
+
|
|
90
|
+
// Stable callback for color change to prevent infinite re-renders
|
|
91
|
+
const stableHandleColorChange = useCallback(
|
|
92
|
+
(key: keyof typeof localCustomColors, color: string) => {
|
|
93
|
+
handleColorChange(key, color);
|
|
94
|
+
},
|
|
95
|
+
[handleColorChange]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Memoize sections to prevent unnecessary re-renders
|
|
99
|
+
const themeSectionMemo = useMemo(() => {
|
|
100
|
+
if (!showThemeSection) return null;
|
|
101
|
+
|
|
102
|
+
// Construct themes from texts prop
|
|
103
|
+
// This adheres to "Package Driven Design" where content is driven by the consumer (App)
|
|
104
|
+
const themes = [];
|
|
105
|
+
|
|
106
|
+
// We only add the theme option if the corresponding text config is provided
|
|
107
|
+
if (texts?.lightMode) {
|
|
108
|
+
themes.push({
|
|
109
|
+
mode: 'light' as const,
|
|
110
|
+
title: texts.lightMode.title,
|
|
111
|
+
subtitle: texts.lightMode.subtitle,
|
|
112
|
+
description: texts.lightMode.description,
|
|
113
|
+
features: texts.lightMode.features
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (texts?.darkMode) {
|
|
118
|
+
themes.push({
|
|
119
|
+
mode: 'dark' as const,
|
|
120
|
+
title: texts.darkMode.title,
|
|
121
|
+
subtitle: texts.darkMode.subtitle,
|
|
122
|
+
description: texts.darkMode.description,
|
|
123
|
+
features: texts.darkMode.features
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// If no texts provided, themes array is empty, section will return null.
|
|
128
|
+
// This forces the consuming app to provide the texts.
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<ThemeModeSection
|
|
132
|
+
tokens={tokens}
|
|
133
|
+
themeMode={themeMode}
|
|
134
|
+
onThemeSelect={handleThemeSelect}
|
|
135
|
+
title={texts?.themeSectionTitle}
|
|
136
|
+
description={texts?.themeSectionDescription}
|
|
137
|
+
themes={themes.length > 0 ? themes : undefined}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
}, [
|
|
141
|
+
showThemeSection,
|
|
142
|
+
tokens,
|
|
143
|
+
themeMode,
|
|
144
|
+
handleThemeSelect,
|
|
145
|
+
texts?.themeSectionTitle,
|
|
146
|
+
texts?.themeSectionDescription,
|
|
147
|
+
texts?.lightMode,
|
|
148
|
+
texts?.darkMode,
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
const colorsSectionMemo = useMemo(() => {
|
|
152
|
+
if (!showColorsSection) return null;
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<CustomColorsSection
|
|
156
|
+
tokens={tokens}
|
|
157
|
+
localCustomColors={localCustomColors}
|
|
158
|
+
onColorChange={stableHandleColorChange}
|
|
159
|
+
onResetColors={handleResetColors}
|
|
160
|
+
title={texts?.colorsSectionTitle}
|
|
161
|
+
description={texts?.colorsSectionDescription}
|
|
162
|
+
resetButtonText={texts?.resetButtonText}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
}, [
|
|
166
|
+
showColorsSection,
|
|
167
|
+
tokens,
|
|
168
|
+
localCustomColors,
|
|
169
|
+
stableHandleColorChange,
|
|
170
|
+
handleResetColors,
|
|
171
|
+
texts?.colorsSectionTitle,
|
|
172
|
+
texts?.colorsSectionDescription,
|
|
173
|
+
texts?.resetButtonText,
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
const previewSectionMemo = useMemo(() => {
|
|
177
|
+
if (!showPreviewSection) return null;
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<AppearancePreview
|
|
181
|
+
tokens={tokens}
|
|
182
|
+
localCustomColors={localCustomColors}
|
|
183
|
+
title={texts?.previewSectionTitle}
|
|
184
|
+
description={texts?.previewSectionDescription}
|
|
185
|
+
/>
|
|
186
|
+
);
|
|
187
|
+
}, [
|
|
188
|
+
showPreviewSection,
|
|
189
|
+
tokens,
|
|
190
|
+
localCustomColors,
|
|
191
|
+
texts?.previewSectionTitle,
|
|
192
|
+
texts?.previewSectionDescription,
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<View style={[styles.container, containerStyle]}>
|
|
197
|
+
<ScrollView
|
|
198
|
+
style={styles.scrollView}
|
|
199
|
+
contentContainerStyle={[styles.scrollContent, contentContainerStyle]}
|
|
200
|
+
showsVerticalScrollIndicator={false}
|
|
201
|
+
removeClippedSubviews={true} // Performance optimization for long lists
|
|
202
|
+
scrollEventThrottle={16} // 60fps throttling
|
|
203
|
+
>
|
|
204
|
+
{headerComponentMemo}
|
|
205
|
+
{themeSectionMemo}
|
|
206
|
+
{colorsSectionMemo}
|
|
207
|
+
{previewSectionMemo}
|
|
208
|
+
</ScrollView>
|
|
209
|
+
</View>
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const getStyles = (tokens: ReturnType<typeof useResponsiveDesignTokens>) =>
|
|
214
|
+
StyleSheet.create({
|
|
215
|
+
container: {
|
|
216
|
+
flex: 1,
|
|
217
|
+
backgroundColor: tokens.colors.backgroundPrimary,
|
|
218
|
+
},
|
|
219
|
+
scrollView: {
|
|
220
|
+
flex: 1,
|
|
221
|
+
},
|
|
222
|
+
scrollContent: {
|
|
223
|
+
padding: tokens.spacing.md,
|
|
224
|
+
paddingBottom: tokens.spacing.xl,
|
|
225
|
+
},
|
|
226
|
+
});
|