@pi-spi/app 1.5.1 → 2.0.0

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 (180) hide show
  1. package/README.md +64 -30
  2. package/example/App.tsx +27 -24
  3. package/lib/module/core/assets.js +0 -7
  4. package/lib/module/core/assets.js.map +1 -1
  5. package/lib/module/core/di.js +17 -8
  6. package/lib/module/core/di.js.map +1 -1
  7. package/lib/module/core/pi_spi_custom_theme.js +22 -10
  8. package/lib/module/core/pi_spi_custom_theme.js.map +1 -1
  9. package/lib/module/core/pi_spi_sdk_config.js +22 -8
  10. package/lib/module/core/pi_spi_sdk_config.js.map +1 -1
  11. package/lib/module/core/theme.js +345 -225
  12. package/lib/module/core/theme.js.map +1 -1
  13. package/lib/module/index.js +1 -1
  14. package/lib/module/index.js.map +1 -1
  15. package/lib/module/l10n/app_localizations.js.map +1 -1
  16. package/lib/module/l10n/app_localizations_en.js +26 -1
  17. package/lib/module/l10n/app_localizations_en.js.map +1 -1
  18. package/lib/module/l10n/app_localizations_fr.js +26 -1
  19. package/lib/module/l10n/app_localizations_fr.js.map +1 -1
  20. package/lib/module/l10n/app_localizations_pt.js +26 -1
  21. package/lib/module/l10n/app_localizations_pt.js.map +1 -1
  22. package/lib/module/modules/alias/presentation/pages/alias_delete_btn_widget.js +15 -19
  23. package/lib/module/modules/alias/presentation/pages/alias_delete_btn_widget.js.map +1 -1
  24. package/lib/module/modules/alias/presentation/pages/alias_page_claim.js +4 -11
  25. package/lib/module/modules/alias/presentation/pages/alias_page_claim.js.map +1 -1
  26. package/lib/module/modules/compte/presentation/pages/compte_details_widget.js +6 -7
  27. package/lib/module/modules/compte/presentation/pages/compte_details_widget.js.map +1 -1
  28. package/lib/module/modules/config/domain/models/config_keys.js +3 -1
  29. package/lib/module/modules/config/domain/models/config_keys.js.map +1 -1
  30. package/lib/module/modules/config/domain/services/config_service.js +31 -2
  31. package/lib/module/modules/config/domain/services/config_service.js.map +1 -1
  32. package/lib/module/modules/home/presentation/pages/home_tab_compte.js +0 -1
  33. package/lib/module/modules/home/presentation/pages/home_tab_compte.js.map +1 -1
  34. package/lib/module/modules/home/presentation/pages/home_tab_compte_more_menu.js +38 -37
  35. package/lib/module/modules/home/presentation/pages/home_tab_compte_more_menu.js.map +1 -1
  36. package/lib/module/modules/profile/presentation/pages/compte/profile_compte_page.js +25 -11
  37. package/lib/module/modules/profile/presentation/pages/compte/profile_compte_page.js.map +1 -1
  38. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js +14 -20
  39. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js.map +1 -1
  40. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.js +109 -103
  41. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.js.map +1 -1
  42. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.js +55 -72
  43. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.js.map +1 -1
  44. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.js +31 -45
  45. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.js.map +1 -1
  46. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js +175 -203
  47. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js.map +1 -1
  48. package/lib/module/modules/profile/presentation/pages/profile_app_bar.js +26 -0
  49. package/lib/module/modules/profile/presentation/pages/profile_app_bar.js.map +1 -0
  50. package/lib/module/modules/profile/presentation/pages/profile_menu.js +144 -34
  51. package/lib/module/modules/profile/presentation/pages/profile_menu.js.map +1 -1
  52. package/lib/module/modules/profile/presentation/pages/profile_page.js +95 -63
  53. package/lib/module/modules/profile/presentation/pages/profile_page.js.map +1 -1
  54. package/lib/module/modules/profile/presentation/pages/profile_page_avatar.js +97 -45
  55. package/lib/module/modules/profile/presentation/pages/profile_page_avatar.js.map +1 -1
  56. package/lib/module/modules/profile/presentation/pages/profile_page_contacts.js +9 -8
  57. package/lib/module/modules/profile/presentation/pages/profile_page_contacts.js.map +1 -1
  58. package/lib/module/modules/search/presentation/pages/search_fonctionnalites.js +26 -2
  59. package/lib/module/modules/search/presentation/pages/search_fonctionnalites.js.map +1 -1
  60. package/lib/module/modules/search/presentation/pages/search_page.js +0 -1
  61. package/lib/module/modules/search/presentation/pages/search_page.js.map +1 -1
  62. package/lib/module/modules/transactions/presentation/pages/transaction_details/transaction_details_page_recu.js +3 -1
  63. package/lib/module/modules/transactions/presentation/pages/transaction_details/transaction_details_page_recu.js.map +1 -1
  64. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.js +4 -11
  65. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.js.map +1 -1
  66. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.js +13 -10
  67. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.js.map +1 -1
  68. package/lib/module/modules/transactions/presentation/pages/transaction_search/transaction_search_page_filters.js +5 -4
  69. package/lib/module/modules/transactions/presentation/pages/transaction_search/transaction_search_page_filters.js.map +1 -1
  70. package/lib/module/modules/transactions/presentation/pages/transaction_split/transaction_split_page_repartition.js +2 -2
  71. package/lib/module/modules/transactions/presentation/pages/transaction_split/transaction_split_page_repartition.js.map +1 -1
  72. package/lib/module/pi_spi_sdk_entry.js +7 -2
  73. package/lib/module/pi_spi_sdk_entry.js.map +1 -1
  74. package/lib/module/shared/utils/color.js +15 -0
  75. package/lib/module/shared/utils/color.js.map +1 -0
  76. package/lib/module/shared/widgets/custom_progressbar.js +3 -2
  77. package/lib/module/shared/widgets/custom_progressbar.js.map +1 -1
  78. package/lib/module/shared/widgets/error_card_widget.js +70 -0
  79. package/lib/module/shared/widgets/error_card_widget.js.map +1 -0
  80. package/lib/module/shared/widgets/selection.js +136 -0
  81. package/lib/module/shared/widgets/selection.js.map +1 -0
  82. package/lib/typescript/src/core/assets.d.ts +0 -7
  83. package/lib/typescript/src/core/assets.d.ts.map +1 -1
  84. package/lib/typescript/src/core/di.d.ts.map +1 -1
  85. package/lib/typescript/src/core/pi_spi_custom_theme.d.ts +17 -8
  86. package/lib/typescript/src/core/pi_spi_custom_theme.d.ts.map +1 -1
  87. package/lib/typescript/src/core/pi_spi_sdk_config.d.ts +22 -8
  88. package/lib/typescript/src/core/pi_spi_sdk_config.d.ts.map +1 -1
  89. package/lib/typescript/src/core/theme.d.ts +123 -53
  90. package/lib/typescript/src/core/theme.d.ts.map +1 -1
  91. package/lib/typescript/src/index.d.ts +1 -1
  92. package/lib/typescript/src/index.d.ts.map +1 -1
  93. package/lib/typescript/src/l10n/app_localizations.d.ts +50 -0
  94. package/lib/typescript/src/l10n/app_localizations.d.ts.map +1 -1
  95. package/lib/typescript/src/l10n/app_localizations_en.d.ts.map +1 -1
  96. package/lib/typescript/src/l10n/app_localizations_fr.d.ts.map +1 -1
  97. package/lib/typescript/src/l10n/app_localizations_pt.d.ts.map +1 -1
  98. package/lib/typescript/src/modules/alias/presentation/pages/alias_delete_btn_widget.d.ts.map +1 -1
  99. package/lib/typescript/src/modules/alias/presentation/pages/alias_page_claim.d.ts.map +1 -1
  100. package/lib/typescript/src/modules/compte/presentation/pages/compte_details_widget.d.ts.map +1 -1
  101. package/lib/typescript/src/modules/config/domain/models/config_keys.d.ts +3 -1
  102. package/lib/typescript/src/modules/config/domain/models/config_keys.d.ts.map +1 -1
  103. package/lib/typescript/src/modules/config/domain/services/config_service.d.ts +7 -0
  104. package/lib/typescript/src/modules/config/domain/services/config_service.d.ts.map +1 -1
  105. package/lib/typescript/src/modules/home/presentation/pages/home_tab_compte_more_menu.d.ts +2 -0
  106. package/lib/typescript/src/modules/home/presentation/pages/home_tab_compte_more_menu.d.ts.map +1 -1
  107. package/lib/typescript/src/modules/profile/presentation/pages/compte/profile_compte_page.d.ts.map +1 -1
  108. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page.d.ts.map +1 -1
  109. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.d.ts.map +1 -1
  110. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.d.ts.map +1 -1
  111. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.d.ts.map +1 -1
  112. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.d.ts +1 -1
  113. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.d.ts.map +1 -1
  114. package/lib/typescript/src/modules/profile/presentation/pages/profile_app_bar.d.ts +5 -0
  115. package/lib/typescript/src/modules/profile/presentation/pages/profile_app_bar.d.ts.map +1 -0
  116. package/lib/typescript/src/modules/profile/presentation/pages/profile_menu.d.ts +31 -4
  117. package/lib/typescript/src/modules/profile/presentation/pages/profile_menu.d.ts.map +1 -1
  118. package/lib/typescript/src/modules/profile/presentation/pages/profile_page.d.ts +4 -1
  119. package/lib/typescript/src/modules/profile/presentation/pages/profile_page.d.ts.map +1 -1
  120. package/lib/typescript/src/modules/profile/presentation/pages/profile_page_avatar.d.ts +1 -1
  121. package/lib/typescript/src/modules/profile/presentation/pages/profile_page_avatar.d.ts.map +1 -1
  122. package/lib/typescript/src/modules/profile/presentation/pages/profile_page_contacts.d.ts.map +1 -1
  123. package/lib/typescript/src/modules/search/presentation/pages/search_fonctionnalites.d.ts.map +1 -1
  124. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.d.ts.map +1 -1
  125. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.d.ts.map +1 -1
  126. package/lib/typescript/src/shared/utils/color.d.ts +7 -0
  127. package/lib/typescript/src/shared/utils/color.d.ts.map +1 -0
  128. package/lib/typescript/src/shared/widgets/custom_progressbar.d.ts.map +1 -1
  129. package/lib/typescript/src/shared/widgets/error_card_widget.d.ts +11 -0
  130. package/lib/typescript/src/shared/widgets/error_card_widget.d.ts.map +1 -0
  131. package/lib/typescript/src/shared/widgets/selection.d.ts +21 -0
  132. package/lib/typescript/src/shared/widgets/selection.d.ts.map +1 -0
  133. package/package.json +1 -1
  134. package/src/core/assets.ts +0 -7
  135. package/src/core/di.ts +13 -8
  136. package/src/core/pi_spi_custom_theme.ts +22 -15
  137. package/src/core/pi_spi_sdk_config.ts +27 -9
  138. package/src/core/theme.ts +381 -269
  139. package/src/index.ts +1 -1
  140. package/src/l10n/app_localizations.ts +50 -0
  141. package/src/l10n/app_localizations_en.ts +25 -0
  142. package/src/l10n/app_localizations_fr.ts +25 -0
  143. package/src/l10n/app_localizations_pt.ts +25 -0
  144. package/src/modules/alias/presentation/pages/alias_delete_btn_widget.tsx +13 -12
  145. package/src/modules/alias/presentation/pages/alias_page_claim.tsx +5 -4
  146. package/src/modules/compte/presentation/pages/compte_details_widget.tsx +3 -4
  147. package/src/modules/config/domain/models/config_keys.ts +3 -1
  148. package/src/modules/config/domain/services/config_service.ts +31 -2
  149. package/src/modules/home/presentation/pages/home_tab_compte.tsx +1 -1
  150. package/src/modules/home/presentation/pages/home_tab_compte_more_menu.tsx +34 -42
  151. package/src/modules/profile/presentation/pages/compte/profile_compte_page.tsx +15 -7
  152. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page.tsx +12 -17
  153. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.tsx +89 -68
  154. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.tsx +84 -54
  155. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.tsx +28 -24
  156. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.tsx +121 -131
  157. package/src/modules/profile/presentation/pages/profile_app_bar.tsx +21 -0
  158. package/src/modules/profile/presentation/pages/profile_menu.tsx +126 -33
  159. package/src/modules/profile/presentation/pages/profile_page.tsx +90 -66
  160. package/src/modules/profile/presentation/pages/profile_page_avatar.tsx +69 -28
  161. package/src/modules/profile/presentation/pages/profile_page_contacts.tsx +5 -4
  162. package/src/modules/search/presentation/pages/search_fonctionnalites.ts +40 -7
  163. package/src/modules/search/presentation/pages/search_page.tsx +1 -1
  164. package/src/modules/transactions/presentation/pages/transaction_details/transaction_details_page_recu.tsx +3 -1
  165. package/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.tsx +2 -4
  166. package/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.tsx +13 -6
  167. package/src/modules/transactions/presentation/pages/transaction_search/transaction_search_page_filters.tsx +4 -4
  168. package/src/modules/transactions/presentation/pages/transaction_split/transaction_split_page_repartition.tsx +2 -2
  169. package/src/pi_spi_sdk_entry.tsx +9 -2
  170. package/src/shared/utils/color.ts +14 -0
  171. package/src/shared/widgets/custom_progressbar.tsx +3 -2
  172. package/src/shared/widgets/error_card_widget.tsx +58 -0
  173. package/src/shared/widgets/selection.tsx +98 -0
  174. package/assets/images/theme_light_blue.jpeg +0 -0
  175. package/assets/images/theme_light_green.jpeg +0 -0
  176. package/assets/images/theme_light_orange.jpeg +0 -0
  177. package/assets/images/theme_light_pink.jpeg +0 -0
  178. package/assets/images/theme_light_purple.jpeg +0 -0
  179. package/assets/images/theme_light_red.jpeg +0 -0
  180. package/assets/images/theme_light_yellow.jpeg +0 -0
@@ -1,21 +1,22 @@
1
+ import { MaterialIcons } from '@expo/vector-icons';
1
2
  import { Image } from 'expo-image';
2
- import type { ReactNode } from 'react';
3
+ import { useState } from 'react';
3
4
  import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
4
5
 
5
6
  import { Images } from '../../../../../core/assets';
6
7
  import { useBloc, useBlocBuilder } from '../../../../../core/bloc_react';
7
- import { AppRouter, useAppRouter } from '../../../../../core/router';
8
- import { Themer, Themes, useTheme, type ThemeData } from '../../../../../core/theme';
8
+ import { Accents, Themer, Themes, useTheme } from '../../../../../core/theme';
9
9
  import type { AppLocalizations } from '../../../../../l10n/app_localizations';
10
10
  import { useAppLocalizations } from '../../../../../l10n/localizations';
11
11
  import { Card } from '../../../../../shared/material/card';
12
- import { AppBar, Scaffold } from '../../../../../shared/material/scaffold';
12
+ import { Scaffold } from '../../../../../shared/material/scaffold';
13
13
  import { MyPageContainer } from '../../../../../shared/widgets/my_page_container';
14
14
  import { ConfigBloc } from '../../../../config/adapters/ui/bloc/config_bloc';
15
15
  import { ConfigChangeEvent } from '../../../../config/adapters/ui/bloc/config_event';
16
16
  import { ConfigLoadedState } from '../../../../config/adapters/ui/bloc/config_state';
17
17
  import { ConfigKey } from '../../../../config/domain/models/config_keys';
18
- import { CheckedDot } from './settings_widgets';
18
+ import { ProfileAppBar } from '../profile_app_bar';
19
+ import { ProfileSectionTitre } from '../profile_menu';
19
20
 
20
21
  function themeIcon(theme: Themes): number {
21
22
  switch (theme) {
@@ -23,20 +24,6 @@ function themeIcon(theme: Themes): number {
23
24
  return Images.imageThemeLight;
24
25
  case Themes.dark:
25
26
  return Images.imageThemeDark;
26
- case Themes.yellow:
27
- return Images.imageThemeLightYellow;
28
- case Themes.green:
29
- return Images.imageThemeLightGreen;
30
- case Themes.blue:
31
- return Images.imageThemeLightBlue;
32
- case Themes.red:
33
- return Images.imageThemeLightRed;
34
- case Themes.orange:
35
- return Images.imageThemeLightOrange;
36
- case Themes.purple:
37
- return Images.imageThemeLightPurple;
38
- case Themes.pink:
39
- return Images.imageThemeLightPink;
40
27
  default:
41
28
  return Images.imageThemeSystem;
42
29
  }
@@ -46,46 +33,119 @@ function themeTitle(traductions: AppLocalizations, theme: Themes): string {
46
33
  return theme === Themes.system ? traductions.appSettingPageMenuThemeDefault : Themes.label(theme, traductions);
47
34
  }
48
35
 
49
- /** Choix du thème de l'application (route `profileParametreTheme`). */
36
+ /** Choix du thème (système, clair, sombre) et de la couleur d'accentuation (route `profileParametreTheme`). */
50
37
  export function ProfileParametrePageTheme() {
51
38
  const traductions = useAppLocalizations();
52
39
  const theme = useTheme();
53
- const router = useAppRouter();
54
40
  const configBloc = useBloc(ConfigBloc);
55
41
  const state = useBlocBuilder(
56
42
  configBloc,
57
- (_p, current) => current instanceof ConfigLoadedState && current.updatedKey === ConfigKey.preferedTheme,
43
+ (_p, current) =>
44
+ current instanceof ConfigLoadedState &&
45
+ (current.updatedKey === ConfigKey.preferedTheme || current.updatedKey === ConfigKey.preferedAccent),
58
46
  );
59
- const currentTheme = state.configParams.params[ConfigKey.preferedTheme];
47
+ const currentTheme = Themes.fromCode(state.configParams.params[ConfigKey.preferedTheme]);
48
+ const currentAccent = state.configParams.params[ConfigKey.preferedAccent] ?? Accents.parDefaut;
49
+ const sombre = theme.brightness === 'dark';
50
+ const [largeurGrille, setLargeurGrille] = useState(0);
51
+
52
+ // Trois aperçus par ligne
53
+ const espace = 12;
54
+ // Arrondi vers le bas : sinon la troisième colonne peut passer à la ligne
55
+ const largeur = Math.floor((largeurGrille - 2 * espace) / 3);
60
56
 
61
57
  return (
62
- <Scaffold appBar={<AppBar onBack={() => AppRouter.pop(router)} />}>
58
+ <Scaffold appBar={<ProfileAppBar titre={traductions.appSettingPageMenuThemePageTitle} />}>
63
59
  <MyPageContainer>
64
- <ScrollView>
65
- <Text style={theme.textTheme.titleSmall}>{traductions.appSettingPageMenuThemePageTitle}</Text>
66
- <View style={styles.gap20} />
67
- <Text style={theme.textTheme.headlineMedium}>{traductions.appSettingPageMenuThemeTitle}</Text>
68
- <View style={styles.gap10} />
69
- <Card>
70
- <View style={styles.wrap}>
71
- {/* Thème personnalisé de l'hôte, en tête */}
72
- {Themer.customTheme != null && (
73
- <ThemeOption
74
- title={traductions.appSettingPageMenuThemeCustom}
75
- selected={currentTheme === Themer.customThemeCode}
76
- onSelect={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedTheme, Themer.customThemeCode))}
77
- preview={<CustomThemePreview theme={Themer.customTheme} />}
78
- />
79
- )}
80
- {Themes.list().map((item) => (
81
- <ThemeOption
82
- key={item}
83
- title={themeTitle(traductions, item)}
84
- selected={currentTheme === item}
85
- onSelect={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedTheme, item))}
86
- preview={<Image source={themeIcon(item)} style={styles.flex} contentFit="cover" />}
87
- />
88
- ))}
60
+ <ScrollView contentContainerStyle={styles.contenu}>
61
+ {/* Thème : système, clair ou sombre */}
62
+ <ProfileSectionTitre titre={traductions.appSettingPageThemeSection} />
63
+ <Card style={styles.card}>
64
+ <View style={styles.grille} onLayout={(e) => setLargeurGrille(e.nativeEvent.layout.width - 2 * 14)}>
65
+ {largeurGrille > 0 &&
66
+ Themes.list().map((item) => {
67
+ const choisi = currentTheme === item;
68
+ return (
69
+ <Pressable
70
+ key={item}
71
+ accessibilityRole="button"
72
+ accessibilityState={{ selected: choisi }}
73
+ disabled={choisi}
74
+ onPress={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedTheme, item))}
75
+ style={[styles.item, { width: largeur }]}
76
+ >
77
+ {/* Aperçu, entouré quand il est choisi */}
78
+ <View style={[styles.cadre, { borderColor: choisi ? theme.colorScheme.primary : 'transparent' }]}>
79
+ <Image
80
+ source={themeIcon(item)}
81
+ style={[styles.apercu, { backgroundColor: theme.colorScheme.surface }]}
82
+ contentFit="cover"
83
+ />
84
+ </View>
85
+ {choisi && (
86
+ <View style={[styles.coche, { backgroundColor: theme.colorScheme.primary, borderColor: theme.colorScheme.surface }]}>
87
+ <MaterialIcons name="check" size={14} color={theme.colorScheme.onPrimary} />
88
+ </View>
89
+ )}
90
+ <Text
91
+ style={[
92
+ theme.textTheme.displayMedium,
93
+ styles.libelle,
94
+ { fontWeight: choisi ? '700' : '500' },
95
+ choisi ? null : { color: Themer.neural03Color },
96
+ ]}
97
+ >
98
+ {themeTitle(traductions, item)}
99
+ </Text>
100
+ </Pressable>
101
+ );
102
+ })}
103
+ </View>
104
+ </Card>
105
+
106
+ {/* Couleur d'accentuation */}
107
+ <View style={styles.espace} />
108
+ <ProfileSectionTitre titre={traductions.appSettingPageAccentSection} />
109
+ <Card style={styles.card}>
110
+ <View style={styles.pastilles}>
111
+ {[
112
+ // Couleur du thème personnalisé de l'hôte, en tête
113
+ ...(Themer.hasCustomTheme ? [Themer.customAccentCode] : []),
114
+ ...Accents.list(),
115
+ ].map((accent) => {
116
+ const apercu = Themer.get(sombre ? Themes.dark : Themes.light, accent).colorScheme;
117
+ const teintes = { accent: apercu.primary, surAccent: apercu.onPrimary };
118
+ const choisi = currentAccent === accent;
119
+ return (
120
+ <Pressable
121
+ key={accent}
122
+ accessibilityRole="button"
123
+ accessibilityLabel={Themer.accentLabel(traductions, accent)}
124
+ accessibilityState={{ selected: choisi }}
125
+ disabled={choisi}
126
+ onPress={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedAccent, accent))}
127
+ style={styles.accent}
128
+ >
129
+ {/* Pastille, cochée quand elle est choisie */}
130
+ <View style={[styles.anneau, { borderColor: choisi ? theme.colorScheme.onSurface : 'transparent' }]}>
131
+ <View style={[styles.pastille, { backgroundColor: teintes.accent }]}>
132
+ {choisi && <MaterialIcons name="check" size={22} color={teintes.surAccent} />}
133
+ </View>
134
+ </View>
135
+ <Text
136
+ numberOfLines={1}
137
+ style={[
138
+ theme.textTheme.displaySmall,
139
+ styles.libelle,
140
+ { fontWeight: choisi ? '700' : '400' },
141
+ { color: choisi ? theme.colorScheme.onSurface : Themer.neural03Color },
142
+ ]}
143
+ >
144
+ {Themer.accentLabel(traductions, accent)}
145
+ </Text>
146
+ </Pressable>
147
+ );
148
+ })}
89
149
  </View>
90
150
  </Card>
91
151
  </ScrollView>
@@ -94,88 +154,18 @@ export function ProfileParametrePageTheme() {
94
154
  );
95
155
  }
96
156
 
97
- /** Une option de thème : aperçu, nom et pastille de sélection */
98
- function ThemeOption({
99
- title,
100
- selected,
101
- onSelect,
102
- preview,
103
- }: {
104
- title: string;
105
- selected: boolean;
106
- onSelect: () => void;
107
- preview: ReactNode;
108
- }) {
109
- const theme = useTheme();
110
- return (
111
- <Pressable accessibilityRole="button" disabled={selected} onPress={onSelect} style={styles.item}>
112
- <View style={[styles.preview, { backgroundColor: theme.colorScheme.surface }]}>{preview}</View>
113
- <View style={styles.gap5} />
114
- <Text style={theme.textTheme.displayMedium}>{title}</Text>
115
- <View style={styles.gap5} />
116
- <CheckedDot value={selected} />
117
- <View style={styles.gap20} />
118
- </Pressable>
119
- );
120
- }
121
-
122
- /**
123
- * Aperçu du thème personnalisé de l'hôte (pas d'image comme pour les
124
- * thèmes du SDK) : une miniature d'accueil dessinée avec ses couleurs.
125
- */
126
- function CustomThemePreview({ theme }: { theme: ThemeData }) {
127
- const primary = theme.colorScheme.primary;
128
- const secondary = theme.colorScheme.secondary;
129
- const bar = (width: number, color: string) => <View style={[styles.bar, { width, backgroundColor: color }]} />;
130
- return (
131
- <View style={[styles.flex, styles.mock, { backgroundColor: theme.scaffoldBackgroundColor }]}>
132
- {/* Avatar et titre */}
133
- <View style={styles.mockRow}>
134
- <View style={[styles.dot16, { backgroundColor: secondary }]} />
135
- {bar(40, Themer.neural02Color)}
136
- </View>
137
- <View style={styles.gap12} />
138
- {/* Carte du compte */}
139
- <View style={[styles.mockCard, { backgroundColor: theme.cardColor }]}>
140
- {bar(50, Themer.neural02Color)}
141
- {bar(30, primary)}
142
- </View>
143
- <View style={styles.gap12} />
144
- {/* Boutons d'action */}
145
- <View style={styles.mockActions}>
146
- {[0, 1, 2].map((i) => (
147
- <View key={i} style={[styles.mockButton, { backgroundColor: primary }]} />
148
- ))}
149
- </View>
150
- <View style={styles.gap14} />
151
- {/* Liste */}
152
- {[0, 1, 2].map((i) => (
153
- <View key={i} style={[styles.mockRow, styles.mockListRow]}>
154
- <View style={[styles.dot14, { backgroundColor: secondary }]} />
155
- {bar(46, Themer.neural02Color)}
156
- </View>
157
- ))}
158
- </View>
159
- );
160
- }
161
-
162
157
  const styles = StyleSheet.create({
163
- flex: { flex: 1 },
164
- mock: { padding: 10 },
165
- mockRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
166
- mockListRow: { marginBottom: 8 },
167
- mockCard: { height: 54, padding: 8, borderRadius: 8, justifyContent: 'space-between' },
168
- mockActions: { flexDirection: 'row', justifyContent: 'space-between' },
169
- mockButton: { width: 26, height: 26, borderRadius: 8 },
170
- bar: { height: 6, borderRadius: 3 },
171
- dot16: { width: 16, height: 16, borderRadius: 8 },
172
- dot14: { width: 14, height: 14, borderRadius: 7 },
173
- gap12: { height: 12 },
174
- gap14: { height: 14 },
175
- wrap: { padding: 20, flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', columnGap: 10, rowGap: 10 },
158
+ contenu: { paddingTop: 8, paddingBottom: 16 },
159
+ card: { margin: 0 },
160
+ grille: { padding: 14, flexDirection: 'row', flexWrap: 'wrap', columnGap: 12, rowGap: 16 },
176
161
  item: { alignItems: 'center' },
177
- preview: { width: 120, height: 200, borderRadius: 8, overflow: 'hidden' },
178
- gap5: { height: 5 },
179
- gap10: { height: 10 },
180
- gap20: { height: 20 },
162
+ cadre: { alignSelf: 'stretch', padding: 3, borderWidth: 2, borderRadius: 14 },
163
+ apercu: { width: '100%', aspectRatio: 0.6, borderRadius: 10 },
164
+ coche: { position: 'absolute', top: 8, right: 8, borderRadius: 12, borderWidth: 2, padding: 1 },
165
+ libelle: { marginTop: 8, textAlign: 'center' },
166
+ espace: { height: 24 },
167
+ pastilles: { paddingHorizontal: 10, paddingVertical: 16, flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-evenly', rowGap: 16 },
168
+ accent: { width: 64, alignItems: 'center' },
169
+ anneau: { padding: 3, borderWidth: 2, borderRadius: 26 },
170
+ pastille: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' },
181
171
  });
@@ -0,0 +1,21 @@
1
+ import { Text } from 'react-native';
2
+
3
+ import { AppRouter, useAppRouter } from '../../../../core/router';
4
+ import { useTheme } from '../../../../core/theme';
5
+ import { AppBar } from '../../../../shared/material/scaffold';
6
+
7
+ /** Barre des écrans du profil : bouton retour et [titre] de la page. */
8
+ export function ProfileAppBar({ titre }: { titre: string }) {
9
+ const theme = useTheme();
10
+ const router = useAppRouter();
11
+ return (
12
+ <AppBar
13
+ onBack={() => AppRouter.pop(router)}
14
+ title={
15
+ <Text numberOfLines={1} style={[theme.textTheme.headlineMedium, { color: theme.appBarTheme.foregroundColor }]}>
16
+ {titre}
17
+ </Text>
18
+ }
19
+ />
20
+ );
21
+ }
@@ -1,11 +1,13 @@
1
+ import { MaterialIcons } from '@expo/vector-icons';
1
2
  import { Image } from 'expo-image';
2
- import type { ReactNode } from 'react';
3
- import { Pressable, StyleSheet, Switch, Text, View } from 'react-native';
3
+ import { Fragment, isValidElement, type ComponentProps, type ReactElement, type ReactNode } from 'react';
4
+ import { Platform, Pressable, StyleSheet, Switch, Text, View } from 'react-native';
4
5
 
5
6
  import { AppRouter, useAppRouter } from '../../../../core/router';
6
- import { useTheme } from '../../../../core/theme';
7
+ import { Themer, useTheme } from '../../../../core/theme';
7
8
  import { Card } from '../../../../shared/material/card';
8
9
  import { showModalBottomSheet } from '../../../../shared/material/overlays';
10
+ import { withAlpha } from '../../../../shared/utils/color';
9
11
 
10
12
  /** Une entrée du menu du profil : une route, une action, ou une feuille à ouvrir. */
11
13
  export class ProfileItem {
@@ -27,45 +29,136 @@ export class ProfileItem {
27
29
  }
28
30
  }
29
31
 
30
- /** Menu du profil : une carte avec une ligne (icône, titre, sous-titre) par entrée. */
31
- export function ProfilePageMenu({ items }: { items: ProfileItem[] }) {
32
+ /**
33
+ * Groupe d'entrées du profil dans une carte : [titre] de section facultatif
34
+ * au-dessus, puis une ligne par élément, séparées par un trait. Les [items]
35
+ * sont des `ProfileItem` ou des composants déjà construits (ex. bouton de
36
+ * modification du code PIN, qui utilise `ProfileMenuTile`).
37
+ */
38
+ export function ProfilePageMenu({ items, titre }: { items: (ProfileItem | ReactElement)[]; titre?: string }) {
32
39
  const theme = useTheme();
33
40
  const router = useAppRouter();
34
- return (
35
- <Card>
36
- {items.map((item, i) => (
37
- <Pressable
38
- key={i}
39
- accessibilityRole="button"
41
+ const lignes = items
42
+ .map((item) => {
43
+ if (!(item instanceof ProfileItem)) return isValidElement(item) ? item : null;
44
+ const aSwitch = item.action != null && item.value != null;
45
+ return (
46
+ <ProfileMenuTile
47
+ image={item.icon}
48
+ titre={item.title}
49
+ sousTitre={item.subtitle}
40
50
  onPress={() => {
41
51
  if (typeof item.page === 'string') void AppRouter.push(router, item.page);
42
52
  else if (typeof item.page === 'function') item.page();
43
53
  // Ouvrir une feuille
44
- else if (item.sheet != null) void showModalBottomSheet(router, { themed: true, builder: item.sheet });
54
+ else if (item.sheet != null) void showModalBottomSheet(router, { builder: item.sheet });
45
55
  }}
46
- style={({ pressed }) => [styles.tile, pressed ? { backgroundColor: theme.colorScheme.secondary } : null]}
47
- >
48
- <Image source={item.icon} style={styles.icon} tintColor={theme.colorScheme.onSurface} />
49
- <View style={styles.texts}>
50
- <Text style={theme.textTheme.bodyLarge}>{item.title}</Text>
51
- {item.subtitle != null && <Text style={theme.textTheme.displaySmall}>{item.subtitle}</Text>}
52
- </View>
53
- {item.action != null && item.value != null && (
54
- <Switch
55
- value={item.value}
56
- onValueChange={item.action}
57
- trackColor={{ false: theme.switchTheme.inactiveTrackColor, true: theme.switchTheme.activeTrackColor }}
58
- thumbColor={theme.switchTheme.thumbColor}
59
- />
60
- )}
61
- </Pressable>
62
- ))}
63
- </Card>
56
+ trailing={
57
+ aSwitch ? (
58
+ <Switch
59
+ value={item.value}
60
+ onValueChange={item.action}
61
+ trackColor={{ false: theme.switchTheme.inactiveTrackColor, true: theme.switchTheme.activeTrackColor }}
62
+ thumbColor={theme.switchTheme.thumbColor}
63
+ />
64
+ ) : undefined
65
+ }
66
+ />
67
+ );
68
+ })
69
+ .filter((ligne): ligne is ReactElement => ligne != null);
70
+
71
+ return (
72
+ <View>
73
+ {titre != null && <ProfileSectionTitre titre={titre} />}
74
+ <Card style={styles.card}>
75
+ {lignes.map((ligne, i) => (
76
+ <Fragment key={i}>
77
+ {i > 0 && <View style={[styles.separateur, { backgroundColor: withAlpha(Themer.neural03Color, 0.25) }]} />}
78
+ {ligne}
79
+ </Fragment>
80
+ ))}
81
+ </Card>
82
+ </View>
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Ligne d'un menu du profil : pastille avec l'icône, [titre], [sousTitre]
88
+ * facultatif et, à droite, [trailing] (ex. Switch) ou une flèche.
89
+ * [couleur] remplace la couleur principale (ex. rouge pour une action
90
+ * destructive).
91
+ */
92
+ export function ProfileMenuTile({
93
+ image,
94
+ icone,
95
+ titre,
96
+ sousTitre,
97
+ onPress,
98
+ trailing,
99
+ couleur,
100
+ disabled,
101
+ }: {
102
+ image?: number;
103
+ icone?: ComponentProps<typeof MaterialIcons>['name'];
104
+ titre: string;
105
+ sousTitre?: string;
106
+ onPress?: () => void;
107
+ trailing?: ReactNode;
108
+ couleur?: string;
109
+ disabled?: boolean;
110
+ }) {
111
+ const theme = useTheme();
112
+ const principale = couleur ?? theme.colorScheme.primary;
113
+ return (
114
+ <Pressable
115
+ accessibilityRole="button"
116
+ disabled={disabled}
117
+ onPress={onPress}
118
+ style={({ pressed }) => [styles.tile, pressed ? { backgroundColor: theme.colorScheme.secondary } : null]}
119
+ >
120
+ {/* Pastille */}
121
+ <View style={[styles.pastille, { backgroundColor: withAlpha(principale, 0.14) }]}>
122
+ {image != null ? (
123
+ <Image source={image} style={styles.icon} tintColor={couleur ?? theme.colorScheme.onSurface} />
124
+ ) : (
125
+ <MaterialIcons name={icone ?? 'circle'} size={20} color={principale} />
126
+ )}
127
+ </View>
128
+ <View style={styles.texts}>
129
+ <Text style={[theme.textTheme.displayLarge, styles.titre, couleur != null ? { color: couleur } : null]}>{titre}</Text>
130
+ {sousTitre != null && sousTitre.length > 0 && (
131
+ <Text style={[theme.textTheme.displaySmall, styles.sousTitre, { color: Themer.neural03Color }]}>{sousTitre}</Text>
132
+ )}
133
+ </View>
134
+ {trailing ?? (
135
+ <MaterialIcons
136
+ name={Platform.OS === 'ios' ? 'arrow-forward-ios' : 'arrow-forward'}
137
+ size={18}
138
+ color={couleur ?? Themer.neural03Color}
139
+ />
140
+ )}
141
+ </Pressable>
142
+ );
143
+ }
144
+
145
+ /** Titre d'une section du profil, en petites capitales. */
146
+ export function ProfileSectionTitre({ titre }: { titre: string }) {
147
+ const theme = useTheme();
148
+ return (
149
+ <Text style={[theme.textTheme.displaySmall, styles.section, { color: Themer.neural03Color }]}>{titre.toUpperCase()}</Text>
64
150
  );
65
151
  }
66
152
 
67
153
  const styles = StyleSheet.create({
68
- tile: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, minHeight: 56, paddingVertical: 8 },
69
- icon: { width: 24, height: 24 },
70
- texts: { flex: 1, marginLeft: 16 },
154
+ card: { margin: 0 },
155
+ tile: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12 },
156
+ pastille: { width: 40, height: 40, borderRadius: 12, alignItems: 'center', justifyContent: 'center' },
157
+ icon: { width: 20, height: 20 },
158
+ texts: { flex: 1, marginLeft: 14, marginRight: 8 },
159
+ titre: { fontWeight: '500' },
160
+ sousTitre: { marginTop: 2 },
161
+ section: { fontSize: 12, letterSpacing: 0.8, fontWeight: '600', marginLeft: 4, marginBottom: 8 },
162
+ // Trait aligné sur le texte (après la pastille)
163
+ separateur: { height: StyleSheet.hairlineWidth, marginLeft: 68 },
71
164
  });