@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.
Files changed (103) hide show
  1. package/package.json +16 -15
  2. package/src/domains/about/__tests__/integration.test.tsx +328 -0
  3. package/src/domains/about/__tests__/types.d.ts +5 -0
  4. package/src/domains/about/domain/entities/AppInfo.ts +74 -0
  5. package/src/domains/about/domain/entities/__tests__/AppInfo.test.ts +93 -0
  6. package/src/domains/about/domain/repositories/IAboutRepository.ts +22 -0
  7. package/src/domains/about/index.ts +10 -0
  8. package/src/domains/about/infrastructure/repositories/AboutRepository.ts +68 -0
  9. package/src/domains/about/infrastructure/repositories/__tests__/AboutRepository.test.ts +153 -0
  10. package/src/domains/about/presentation/components/AboutContent.tsx +104 -0
  11. package/src/domains/about/presentation/components/AboutHeader.tsx +79 -0
  12. package/src/domains/about/presentation/components/AboutSection.tsx +134 -0
  13. package/src/domains/about/presentation/components/AboutSettingItem.tsx +208 -0
  14. package/src/domains/about/presentation/components/__tests__/AboutContent.simple.test.tsx +178 -0
  15. package/src/domains/about/presentation/components/__tests__/AboutContent.test.tsx +293 -0
  16. package/src/domains/about/presentation/components/__tests__/AboutHeader.test.tsx +201 -0
  17. package/src/domains/about/presentation/components/__tests__/AboutSettingItem.test.tsx +71 -0
  18. package/src/domains/about/presentation/hooks/__tests__/useAboutInfo.simple.test.tsx +229 -0
  19. package/src/domains/about/presentation/hooks/__tests__/useAboutInfo.test.tsx +240 -0
  20. package/src/domains/about/presentation/hooks/useAboutInfo.ts +262 -0
  21. package/src/domains/about/presentation/screens/AboutScreen.tsx +195 -0
  22. package/src/domains/about/presentation/screens/__tests__/AboutScreen.simple.test.tsx +199 -0
  23. package/src/domains/about/presentation/screens/__tests__/AboutScreen.test.tsx +366 -0
  24. package/src/domains/about/types/global.d.ts +15 -0
  25. package/src/domains/about/utils/__tests__/index.test.ts +408 -0
  26. package/src/domains/about/utils/index.ts +160 -0
  27. package/src/domains/appearance/__tests__/components/AppearanceScreen.test.tsx +195 -0
  28. package/src/domains/appearance/__tests__/hooks/index.test.tsx +232 -0
  29. package/src/domains/appearance/__tests__/integration/index.test.tsx +207 -0
  30. package/src/domains/appearance/__tests__/services/appearanceService.test.ts +299 -0
  31. package/src/domains/appearance/__tests__/setup.ts +96 -0
  32. package/src/domains/appearance/__tests__/stores/appearanceStore.test.tsx +175 -0
  33. package/src/domains/appearance/data/colorPalettes.ts +94 -0
  34. package/src/domains/appearance/hooks/index.ts +6 -0
  35. package/src/domains/appearance/hooks/useAppearance.ts +61 -0
  36. package/src/domains/appearance/hooks/useAppearanceActions.ts +144 -0
  37. package/src/domains/appearance/index.ts +7 -0
  38. package/src/domains/appearance/infrastructure/services/appearanceService.ts +301 -0
  39. package/src/domains/appearance/infrastructure/services/systemThemeDetection.ts +79 -0
  40. package/src/domains/appearance/infrastructure/services/validation.ts +91 -0
  41. package/src/domains/appearance/infrastructure/storage/appearanceStorage.ts +120 -0
  42. package/src/domains/appearance/infrastructure/stores/appearanceStore.ts +132 -0
  43. package/src/domains/appearance/presentation/components/AppearanceHeader.tsx +67 -0
  44. package/src/domains/appearance/presentation/components/AppearancePreview.tsx +141 -0
  45. package/src/domains/appearance/presentation/components/AppearanceSection.tsx +139 -0
  46. package/src/domains/appearance/presentation/components/ColorPicker.tsx +113 -0
  47. package/src/domains/appearance/presentation/components/CustomColorsSection.tsx +186 -0
  48. package/src/domains/appearance/presentation/components/ThemeModeSection.tsx +110 -0
  49. package/src/domains/appearance/presentation/components/ThemeOption.tsx +138 -0
  50. package/src/domains/appearance/presentation/components/index.ts +6 -0
  51. package/src/domains/appearance/presentation/screens/AppearanceScreen.tsx +226 -0
  52. package/src/domains/appearance/presentation/screens/index.ts +2 -0
  53. package/src/domains/appearance/types/index.ts +54 -0
  54. package/src/domains/faqs/domain/entities/FAQEntity.ts +16 -0
  55. package/src/domains/faqs/domain/services/FAQSearchService.ts +36 -0
  56. package/src/domains/faqs/domain/services/index.ts +1 -0
  57. package/src/domains/faqs/index.ts +7 -0
  58. package/src/domains/faqs/presentation/components/FAQCategory.tsx +71 -0
  59. package/src/domains/faqs/presentation/components/FAQEmptyState.tsx +75 -0
  60. package/src/domains/faqs/presentation/components/FAQItem.tsx +103 -0
  61. package/src/domains/faqs/presentation/components/FAQSearchBar.tsx +70 -0
  62. package/src/domains/faqs/presentation/components/FAQSection.tsx +50 -0
  63. package/src/domains/faqs/presentation/components/index.ts +18 -0
  64. package/src/domains/faqs/presentation/hooks/index.ts +6 -0
  65. package/src/domains/faqs/presentation/hooks/useFAQExpansion.ts +51 -0
  66. package/src/domains/faqs/presentation/hooks/useFAQSearch.ts +33 -0
  67. package/src/domains/faqs/presentation/screens/FAQScreen.tsx +129 -0
  68. package/src/domains/faqs/presentation/screens/index.ts +2 -0
  69. package/src/domains/feedback/domain/entities/FeedbackEntity.ts +92 -0
  70. package/src/domains/feedback/domain/repositories/IFeedbackRepository.ts +28 -0
  71. package/src/domains/feedback/index.ts +6 -0
  72. package/src/domains/feedback/presentation/components/FeedbackForm.tsx +189 -0
  73. package/src/domains/feedback/presentation/components/FeedbackModal.tsx +111 -0
  74. package/src/domains/feedback/presentation/components/SupportSection.tsx +160 -0
  75. package/src/domains/feedback/presentation/hooks/useDeleteFeedback.ts +25 -0
  76. package/src/domains/feedback/presentation/hooks/useFeedbackForm.ts +59 -0
  77. package/src/domains/feedback/presentation/hooks/useSubmitFeedback.ts +55 -0
  78. package/src/domains/feedback/presentation/hooks/useUserFeedback.ts +29 -0
  79. package/src/domains/legal/__tests__/ContentValidationService.test.ts +195 -0
  80. package/src/domains/legal/__tests__/StyleCacheService.test.ts +110 -0
  81. package/src/domains/legal/__tests__/UrlHandlerService.test.ts +71 -0
  82. package/src/domains/legal/__tests__/setup.ts +82 -0
  83. package/src/domains/legal/domain/entities/LegalConfig.ts +26 -0
  84. package/src/domains/legal/domain/services/ContentValidationService.ts +89 -0
  85. package/src/domains/legal/domain/services/StyleCacheService.ts +97 -0
  86. package/src/domains/legal/domain/services/UrlHandlerService.ts +128 -0
  87. package/src/domains/legal/index.ts +8 -0
  88. package/src/domains/legal/presentation/components/LegalItem.tsx +177 -0
  89. package/src/domains/legal/presentation/components/LegalLinks.tsx +154 -0
  90. package/src/domains/legal/presentation/components/LegalSection.tsx +134 -0
  91. package/src/domains/legal/presentation/screens/LegalScreen.tsx +237 -0
  92. package/src/domains/legal/presentation/screens/PrivacyPolicyScreen.tsx +214 -0
  93. package/src/domains/legal/presentation/screens/TermsOfServiceScreen.tsx +214 -0
  94. package/src/index.ts +19 -0
  95. package/src/presentation/components/DevSettingsSection.tsx +2 -2
  96. package/src/presentation/components/SettingItem.tsx +2 -2
  97. package/src/presentation/components/SettingsErrorBoundary.tsx +2 -2
  98. package/src/presentation/components/SettingsFooter.tsx +2 -2
  99. package/src/presentation/components/SettingsSection.tsx +2 -2
  100. package/src/presentation/navigation/SettingsStackNavigator.tsx +2 -2
  101. package/src/presentation/screens/SettingsScreen.tsx +2 -2
  102. package/src/presentation/screens/components/SettingsContent.tsx +2 -2
  103. 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
+ });
@@ -0,0 +1,2 @@
1
+ export { AppearanceScreen } from "./AppearanceScreen";
2
+ export type { AppearanceScreenProps } from "./AppearanceScreen";