@pi-spi/app 1.5.0 → 1.6.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 (117) hide show
  1. package/README.md +3 -0
  2. package/lib/module/core/api.js +9 -0
  3. package/lib/module/core/api.js.map +1 -1
  4. package/lib/module/core/pi_spi_sdk_config.js +10 -0
  5. package/lib/module/core/pi_spi_sdk_config.js.map +1 -1
  6. package/lib/module/l10n/app_localizations.js.map +1 -1
  7. package/lib/module/l10n/app_localizations_en.js +12 -1
  8. package/lib/module/l10n/app_localizations_en.js.map +1 -1
  9. package/lib/module/l10n/app_localizations_fr.js +12 -1
  10. package/lib/module/l10n/app_localizations_fr.js.map +1 -1
  11. package/lib/module/l10n/app_localizations_pt.js +12 -1
  12. package/lib/module/l10n/app_localizations_pt.js.map +1 -1
  13. package/lib/module/modules/alias/presentation/pages/alias_delete_btn_widget.js +15 -19
  14. package/lib/module/modules/alias/presentation/pages/alias_delete_btn_widget.js.map +1 -1
  15. package/lib/module/modules/alias/presentation/pages/alias_page_claim.js +4 -11
  16. package/lib/module/modules/alias/presentation/pages/alias_page_claim.js.map +1 -1
  17. package/lib/module/modules/compte/presentation/pages/compte_details_widget.js +6 -7
  18. package/lib/module/modules/compte/presentation/pages/compte_details_widget.js.map +1 -1
  19. package/lib/module/modules/profile/presentation/pages/compte/profile_compte_page.js +25 -11
  20. package/lib/module/modules/profile/presentation/pages/compte/profile_compte_page.js.map +1 -1
  21. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js +12 -18
  22. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js.map +1 -1
  23. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.js +109 -103
  24. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.js.map +1 -1
  25. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.js +55 -72
  26. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.js.map +1 -1
  27. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.js +31 -45
  28. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.js.map +1 -1
  29. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js +103 -67
  30. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js.map +1 -1
  31. package/lib/module/modules/profile/presentation/pages/profile_app_bar.js +26 -0
  32. package/lib/module/modules/profile/presentation/pages/profile_app_bar.js.map +1 -0
  33. package/lib/module/modules/profile/presentation/pages/profile_menu.js +143 -34
  34. package/lib/module/modules/profile/presentation/pages/profile_menu.js.map +1 -1
  35. package/lib/module/modules/profile/presentation/pages/profile_page.js +95 -63
  36. package/lib/module/modules/profile/presentation/pages/profile_page.js.map +1 -1
  37. package/lib/module/modules/profile/presentation/pages/profile_page_avatar.js +97 -45
  38. package/lib/module/modules/profile/presentation/pages/profile_page_avatar.js.map +1 -1
  39. package/lib/module/modules/profile/presentation/pages/profile_page_contacts.js +9 -8
  40. package/lib/module/modules/profile/presentation/pages/profile_page_contacts.js.map +1 -1
  41. package/lib/module/modules/search/presentation/pages/search_fonctionnalites.js +26 -2
  42. package/lib/module/modules/search/presentation/pages/search_fonctionnalites.js.map +1 -1
  43. package/lib/module/modules/search/presentation/pages/search_page.js +0 -1
  44. package/lib/module/modules/search/presentation/pages/search_page.js.map +1 -1
  45. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.js +4 -11
  46. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.js.map +1 -1
  47. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.js +13 -10
  48. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.js.map +1 -1
  49. package/lib/module/shared/utils/color.js +15 -0
  50. package/lib/module/shared/utils/color.js.map +1 -0
  51. package/lib/module/shared/widgets/error_card_widget.js +70 -0
  52. package/lib/module/shared/widgets/error_card_widget.js.map +1 -0
  53. package/lib/module/shared/widgets/selection.js +136 -0
  54. package/lib/module/shared/widgets/selection.js.map +1 -0
  55. package/lib/typescript/src/core/api.d.ts.map +1 -1
  56. package/lib/typescript/src/core/pi_spi_sdk_config.d.ts +10 -0
  57. package/lib/typescript/src/core/pi_spi_sdk_config.d.ts.map +1 -1
  58. package/lib/typescript/src/l10n/app_localizations.d.ts +22 -0
  59. package/lib/typescript/src/l10n/app_localizations.d.ts.map +1 -1
  60. package/lib/typescript/src/l10n/app_localizations_en.d.ts.map +1 -1
  61. package/lib/typescript/src/l10n/app_localizations_fr.d.ts.map +1 -1
  62. package/lib/typescript/src/l10n/app_localizations_pt.d.ts.map +1 -1
  63. package/lib/typescript/src/modules/alias/presentation/pages/alias_delete_btn_widget.d.ts.map +1 -1
  64. package/lib/typescript/src/modules/alias/presentation/pages/alias_page_claim.d.ts.map +1 -1
  65. package/lib/typescript/src/modules/compte/presentation/pages/compte_details_widget.d.ts.map +1 -1
  66. package/lib/typescript/src/modules/profile/presentation/pages/compte/profile_compte_page.d.ts.map +1 -1
  67. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page.d.ts.map +1 -1
  68. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.d.ts.map +1 -1
  69. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.d.ts.map +1 -1
  70. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.d.ts.map +1 -1
  71. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.d.ts.map +1 -1
  72. package/lib/typescript/src/modules/profile/presentation/pages/profile_app_bar.d.ts +5 -0
  73. package/lib/typescript/src/modules/profile/presentation/pages/profile_app_bar.d.ts.map +1 -0
  74. package/lib/typescript/src/modules/profile/presentation/pages/profile_menu.d.ts +31 -4
  75. package/lib/typescript/src/modules/profile/presentation/pages/profile_menu.d.ts.map +1 -1
  76. package/lib/typescript/src/modules/profile/presentation/pages/profile_page.d.ts +4 -1
  77. package/lib/typescript/src/modules/profile/presentation/pages/profile_page.d.ts.map +1 -1
  78. package/lib/typescript/src/modules/profile/presentation/pages/profile_page_avatar.d.ts +1 -1
  79. package/lib/typescript/src/modules/profile/presentation/pages/profile_page_avatar.d.ts.map +1 -1
  80. package/lib/typescript/src/modules/profile/presentation/pages/profile_page_contacts.d.ts.map +1 -1
  81. package/lib/typescript/src/modules/search/presentation/pages/search_fonctionnalites.d.ts.map +1 -1
  82. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.d.ts.map +1 -1
  83. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.d.ts.map +1 -1
  84. package/lib/typescript/src/shared/utils/color.d.ts +7 -0
  85. package/lib/typescript/src/shared/utils/color.d.ts.map +1 -0
  86. package/lib/typescript/src/shared/widgets/error_card_widget.d.ts +11 -0
  87. package/lib/typescript/src/shared/widgets/error_card_widget.d.ts.map +1 -0
  88. package/lib/typescript/src/shared/widgets/selection.d.ts +21 -0
  89. package/lib/typescript/src/shared/widgets/selection.d.ts.map +1 -0
  90. package/package.json +1 -1
  91. package/src/core/api.ts +4 -0
  92. package/src/core/pi_spi_sdk_config.ts +14 -0
  93. package/src/l10n/app_localizations.ts +22 -0
  94. package/src/l10n/app_localizations_en.ts +11 -0
  95. package/src/l10n/app_localizations_fr.ts +11 -0
  96. package/src/l10n/app_localizations_pt.ts +11 -0
  97. package/src/modules/alias/presentation/pages/alias_delete_btn_widget.tsx +13 -12
  98. package/src/modules/alias/presentation/pages/alias_page_claim.tsx +5 -4
  99. package/src/modules/compte/presentation/pages/compte_details_widget.tsx +3 -4
  100. package/src/modules/profile/presentation/pages/compte/profile_compte_page.tsx +15 -7
  101. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page.tsx +10 -12
  102. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.tsx +89 -68
  103. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.tsx +84 -54
  104. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.tsx +28 -24
  105. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.tsx +77 -44
  106. package/src/modules/profile/presentation/pages/profile_app_bar.tsx +21 -0
  107. package/src/modules/profile/presentation/pages/profile_menu.tsx +126 -33
  108. package/src/modules/profile/presentation/pages/profile_page.tsx +90 -66
  109. package/src/modules/profile/presentation/pages/profile_page_avatar.tsx +69 -28
  110. package/src/modules/profile/presentation/pages/profile_page_contacts.tsx +5 -4
  111. package/src/modules/search/presentation/pages/search_fonctionnalites.ts +40 -7
  112. package/src/modules/search/presentation/pages/search_page.tsx +1 -1
  113. package/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.tsx +2 -4
  114. package/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.tsx +13 -6
  115. package/src/shared/utils/color.ts +14 -0
  116. package/src/shared/widgets/error_card_widget.tsx +58 -0
  117. package/src/shared/widgets/selection.tsx +98 -0
@@ -1,6 +1,6 @@
1
1
  import { MaterialIcons } from '@expo/vector-icons';
2
- import { useState } from 'react';
3
- import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
2
+ import { useState, type ComponentProps } from 'react';
3
+ import { ScrollView, StyleSheet, Text, View } from 'react-native';
4
4
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
5
 
6
6
  import { Images } from '../../../../../core/assets';
@@ -10,11 +10,12 @@ import { SoundManager } from '../../../../../core/sounds';
10
10
  import { useTheme } from '../../../../../core/theme';
11
11
  import { useAppLocalizations } from '../../../../../l10n/localizations';
12
12
  import { TextButton } from '../../../../../shared/material/buttons';
13
- import { Card } from '../../../../../shared/material/card';
13
+ import { AppBar } from '../../../../../shared/material/scaffold';
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 { ConfigKey } from '../../../../config/domain/models/config_keys';
17
- import { SettingToggle } from './settings_widgets';
17
+ import { ProfileMenuTile, ProfilePageMenu } from '../profile_menu';
18
+ import { SwitchThemed } from './settings_widgets';
18
19
 
19
20
  const NONE = 'NONE';
20
21
 
@@ -44,77 +45,106 @@ export function ProfileParametrePageNotificationsSon() {
44
45
  }
45
46
  };
46
47
 
48
+ const basculer = () => setSelectedSound(enabled ? NONE : (SoundManager.liste[0]?.path ?? null));
49
+
47
50
  return (
48
51
  <View style={[styles.flex, { paddingBottom: insets.bottom }]}>
49
- <View style={styles.bar}>
50
- <Pressable accessibilityRole="button" hitSlop={12} style={styles.back} onPress={() => AppRouter.pop(router)}>
51
- <MaterialIcons name="arrow-back" size={24} color={theme.colorScheme.onSurface} />
52
- </Pressable>
53
- <View style={styles.save}>
54
- <TextButton onPress={save}>{traductions.btnTextSave}</TextButton>
55
- </View>
56
- </View>
52
+ {/* Titre dans la barre, comme les autres pages (dans la feuille : fond de la feuille) */}
53
+ <AppBar
54
+ backgroundColor="transparent"
55
+ onBack={() => AppRouter.pop(router)}
56
+ title={
57
+ <Text numberOfLines={1} style={theme.textTheme.headlineMedium}>
58
+ {traductions.appSettingPageMenuNotificationSonPageTitle}
59
+ </Text>
60
+ }
61
+ actions={
62
+ <View style={styles.save}>
63
+ <TextButton onPress={save}>{traductions.btnTextSave}</TextButton>
64
+ </View>
65
+ }
66
+ />
57
67
  <ScrollView style={styles.flex} contentContainerStyle={styles.content}>
58
- <Text style={theme.textTheme.headlineMedium}>{traductions.appSettingPageMenuNotificationSonPageTitle}</Text>
59
- <View style={styles.gap20} />
60
68
  {/* Son activé ou non */}
61
- <SettingToggle
62
- icon={Images.iconsCloche}
63
- title={traductions.notificationPageTitle}
64
- value={enabled}
65
- onChange={(value) => setSelectedSound(value ? (SoundManager.liste[0]?.path ?? null) : NONE)}
69
+ <ProfilePageMenu
70
+ items={[
71
+ <ProfileMenuTile
72
+ key="son"
73
+ image={Images.iconsCloche}
74
+ titre={traductions.notificationPageTitle}
75
+ onPress={basculer}
76
+ trailing={<SwitchThemed value={enabled} onChange={basculer} />}
77
+ />,
78
+ ]}
66
79
  />
80
+
67
81
  {/* Sons disponibles */}
68
- <View style={styles.gap20} />
69
- <Text style={theme.textTheme.headlineMedium}>{traductions.appSettingPageMenuNotificationSonTitle}</Text>
70
- <View style={styles.gap10} />
71
- <Card>
72
- {/* Aucun */}
73
- <SoundRow
74
- label={traductions.appSettingPageMenuNotificationStyleNone}
75
- checked={selectedSound === NONE}
76
- onPress={() => setSelectedSound(NONE)}
77
- />
78
- {SoundManager.liste.map((sound) => (
82
+ <View style={styles.gap24} />
83
+ <ProfilePageMenu
84
+ titre={traductions.appSettingPageMenuNotificationSonTitle}
85
+ items={[
86
+ // Aucun
79
87
  <SoundRow
80
- key={sound.path}
81
- label={sound.name}
82
- checked={selectedSound === sound.path}
83
- onPress={() => {
84
- void SoundManager.playSound(sound.path);
85
- setSelectedSound(sound.path);
86
- }}
87
- />
88
- ))}
89
- </Card>
88
+ key={NONE}
89
+ icone="music-off"
90
+ label={traductions.appSettingPageMenuNotificationStyleNone}
91
+ checked={selectedSound === NONE}
92
+ onPress={() => setSelectedSound(NONE)}
93
+ />,
94
+ ...SoundManager.liste.map((sound) => (
95
+ <SoundRow
96
+ key={sound.path}
97
+ icone="music-note"
98
+ label={sound.name}
99
+ checked={selectedSound === sound.path}
100
+ onPress={() => {
101
+ void SoundManager.playSound(sound.path);
102
+ setSelectedSound(sound.path);
103
+ }}
104
+ />
105
+ )),
106
+ ]}
107
+ />
90
108
  <View style={styles.gap20} />
91
109
  </ScrollView>
92
110
  </View>
93
111
  );
94
112
  }
95
113
 
96
- function SoundRow({ label, checked, onPress }: { label: string; checked: boolean; onPress: () => void }) {
114
+ /** Son de la liste, coché quand il est choisi */
115
+ function SoundRow({
116
+ icone,
117
+ label,
118
+ checked,
119
+ onPress,
120
+ }: {
121
+ icone: ComponentProps<typeof MaterialIcons>['name'];
122
+ label: string;
123
+ checked: boolean;
124
+ onPress: () => void;
125
+ }) {
97
126
  const theme = useTheme();
98
127
  return (
99
- <Pressable
100
- accessibilityRole="radio"
101
- accessibilityState={{ checked }}
128
+ <ProfileMenuTile
129
+ icone={icone}
130
+ titre={label}
102
131
  onPress={onPress}
103
- style={({ pressed }) => [styles.row, pressed ? { backgroundColor: theme.colorScheme.secondary } : null]}
104
- >
105
- <Text style={[theme.textTheme.bodyLarge, styles.flex]}>{label}</Text>
106
- {checked && <MaterialIcons name="check" size={18} color={theme.colorScheme.onSurface} />}
107
- </Pressable>
132
+ trailing={
133
+ checked ? (
134
+ <MaterialIcons name="check-circle" size={22} color={theme.colorScheme.primary} />
135
+ ) : (
136
+ <View style={styles.vide} />
137
+ )
138
+ }
139
+ />
108
140
  );
109
141
  }
110
142
 
111
143
  const styles = StyleSheet.create({
112
144
  flex: { flex: 1 },
113
- bar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
114
- back: { padding: 12 },
115
145
  save: { paddingRight: 12 },
116
- content: { paddingTop: 12, paddingHorizontal: 12 },
117
- row: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, minHeight: 52 },
118
- gap10: { height: 10 },
146
+ content: { paddingTop: 8, paddingHorizontal: 20 },
147
+ vide: { width: 22 },
119
148
  gap20: { height: 20 },
149
+ gap24: { height: 24 },
120
150
  });
@@ -1,52 +1,56 @@
1
- import { ScrollView, StyleSheet, Text, useWindowDimensions, View } from 'react-native';
2
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
1
+ import { StyleSheet, Text, View } from 'react-native';
3
2
 
4
3
  import { Images } from '../../../../../core/assets';
5
4
  import { useBloc, useBlocBuilder } from '../../../../../core/bloc_react';
6
- import { useTheme } from '../../../../../core/theme';
5
+ import { Themer, useTheme } from '../../../../../core/theme';
7
6
  import { useAppLocalizations } from '../../../../../l10n/localizations';
7
+ import { SbSelectionSheet } from '../../../../../shared/widgets/selection';
8
8
  import { ConfigBloc } from '../../../../config/adapters/ui/bloc/config_bloc';
9
9
  import { ConfigChangeEvent } from '../../../../config/adapters/ui/bloc/config_event';
10
10
  import { ConfigLoadedState } from '../../../../config/adapters/ui/bloc/config_state';
11
11
  import { ConfigKey } from '../../../../config/domain/models/config_keys';
12
- import { SettingToggle } from './settings_widgets';
12
+ import { ProfileMenuTile, ProfilePageMenu } from '../profile_menu';
13
+ import { SwitchThemed } from './settings_widgets';
13
14
 
14
15
  /** Feuille « Mon QR code » : afficher ou non le QR code de l'alias sur l'accueil. */
15
16
  export function ProfileParametrePageQRcode() {
16
17
  const traductions = useAppLocalizations();
17
18
  const theme = useTheme();
18
- const insets = useSafeAreaInsets();
19
- const { height } = useWindowDimensions();
20
19
  const configBloc = useBloc(ConfigBloc);
21
20
  const state = useBlocBuilder(
22
21
  configBloc,
23
22
  (_p, current) => current instanceof ConfigLoadedState && current.updatedKey === ConfigKey.showQRcode,
24
23
  );
25
24
  const showQRcode = state.configParams.params[ConfigKey.showQRcode] !== '0';
25
+ const basculer = () => configBloc.add(new ConfigChangeEvent(ConfigKey.showQRcode, showQRcode ? '0' : '1'));
26
26
 
27
27
  return (
28
- <View style={[styles.box, { backgroundColor: theme.colorScheme.surface, maxHeight: height * 0.5 }]}>
29
- <ScrollView contentContainerStyle={[styles.content, { paddingBottom: 20 + insets.bottom }]}>
30
- <Text style={theme.textTheme.headlineLarge}>{traductions.appSettingPageMenuQrCodeTitle}</Text>
31
- <View style={styles.gap8} />
32
- <Text style={theme.textTheme.displaySmall}>
28
+ <SbSelectionSheet
29
+ titre={traductions.appSettingPageMenuQrCodeTitle}
30
+ entete={
31
+ <Text style={[theme.textTheme.displaySmall, { color: Themer.neural03Color }]}>
33
32
  {showQRcode ? traductions.appSettingPageMenuQrCodeDeselectTitle : traductions.appSettingPageMenuQrCodeSelectTitle}
34
33
  </Text>
35
- <View style={styles.gap15} />
36
- <SettingToggle
37
- icon={Images.homeFooterQrcode}
38
- title={traductions.appSettingPageMenuQrCodeTitle}
39
- value={showQRcode}
40
- onChange={() => configBloc.add(new ConfigChangeEvent(ConfigKey.showQRcode, showQRcode ? '0' : '1'))}
41
- />
42
- </ScrollView>
43
- </View>
34
+ }
35
+ contenu={
36
+ <View style={styles.contenu}>
37
+ <ProfilePageMenu
38
+ items={[
39
+ <ProfileMenuTile
40
+ key="qrcode"
41
+ image={Images.homeFooterQrcode}
42
+ titre={traductions.appSettingPageMenuQrCodeTitle}
43
+ onPress={basculer}
44
+ trailing={<SwitchThemed value={showQRcode} onChange={basculer} />}
45
+ />,
46
+ ]}
47
+ />
48
+ </View>
49
+ }
50
+ />
44
51
  );
45
52
  }
46
53
 
47
54
  const styles = StyleSheet.create({
48
- box: { borderRadius: 20, overflow: 'hidden' },
49
- content: { padding: 20 },
50
- gap8: { height: 8 },
51
- gap15: { height: 15 },
55
+ contenu: { paddingHorizontal: 20, paddingTop: 8, paddingBottom: 20 },
52
56
  });
@@ -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, type ReactNode } 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
8
  import { Themer, Themes, useTheme, type ThemeData } 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) {
@@ -49,8 +50,6 @@ function themeTitle(traductions: AppLocalizations, theme: Themes): string {
49
50
  /** Choix du thème de l'application (route `profileParametreTheme`). */
50
51
  export function ProfileParametrePageTheme() {
51
52
  const traductions = useAppLocalizations();
52
- const theme = useTheme();
53
- const router = useAppRouter();
54
53
  const configBloc = useBloc(ConfigBloc);
55
54
  const state = useBlocBuilder(
56
55
  configBloc,
@@ -58,34 +57,43 @@ export function ProfileParametrePageTheme() {
58
57
  );
59
58
  const currentTheme = state.configParams.params[ConfigKey.preferedTheme];
60
59
 
60
+ const [largeurGrille, setLargeurGrille] = useState(0);
61
+ // Trois aperçus par ligne
62
+ // Arrondi vers le bas : sinon la troisième colonne peut passer à la ligne
63
+ const largeur = Math.floor((largeurGrille - 2 * ESPACE) / 3);
64
+
61
65
  return (
62
- <Scaffold appBar={<AppBar onBack={() => AppRouter.pop(router)} />}>
66
+ <Scaffold appBar={<ProfileAppBar titre={traductions.appSettingPageMenuThemePageTitle} />}>
63
67
  <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
- />
68
+ <ScrollView contentContainerStyle={styles.contenu}>
69
+ {/* Thème */}
70
+ <ProfileSectionTitre titre={traductions.appSettingPageMenuThemeTitle} />
71
+ <Card style={styles.card}>
72
+ <View style={styles.grille} onLayout={(e) => setLargeurGrille(e.nativeEvent.layout.width - 2 * 14)}>
73
+ {largeurGrille > 0 && (
74
+ <>
75
+ {/* Thème personnalisé de l'hôte, en tête */}
76
+ {Themer.customTheme != null && (
77
+ <ThemeOption
78
+ largeur={largeur}
79
+ title={traductions.appSettingPageMenuThemeCustom}
80
+ selected={currentTheme === Themer.customThemeCode}
81
+ onSelect={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedTheme, Themer.customThemeCode))}
82
+ preview={<CustomThemePreview theme={Themer.customTheme} />}
83
+ />
84
+ )}
85
+ {Themes.list().map((item) => (
86
+ <ThemeOption
87
+ key={item}
88
+ largeur={largeur}
89
+ title={themeTitle(traductions, item)}
90
+ selected={currentTheme === item}
91
+ onSelect={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedTheme, item))}
92
+ preview={<Image source={themeIcon(item)} style={styles.flex} contentFit="cover" />}
93
+ />
94
+ ))}
95
+ </>
79
96
  )}
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
- ))}
89
97
  </View>
90
98
  </Card>
91
99
  </ScrollView>
@@ -94,13 +102,15 @@ export function ProfileParametrePageTheme() {
94
102
  );
95
103
  }
96
104
 
97
- /** Une option de thème : aperçu, nom et pastille de sélection */
105
+ /** Une option de thème : aperçu, entouré et coché quand il est choisi, puis son nom */
98
106
  function ThemeOption({
107
+ largeur,
99
108
  title,
100
109
  selected,
101
110
  onSelect,
102
111
  preview,
103
112
  }: {
113
+ largeur: number;
104
114
  title: string;
105
115
  selected: boolean;
106
116
  onSelect: () => void;
@@ -108,13 +118,32 @@ function ThemeOption({
108
118
  }) {
109
119
  const theme = useTheme();
110
120
  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} />
121
+ <Pressable
122
+ accessibilityRole="button"
123
+ accessibilityState={{ selected }}
124
+ disabled={selected}
125
+ onPress={onSelect}
126
+ style={[styles.item, { width: largeur }]}
127
+ >
128
+ {/* Aperçu, entouré quand il est choisi */}
129
+ <View style={[styles.cadre, { borderColor: selected ? theme.colorScheme.primary : 'transparent' }]}>
130
+ <View style={[styles.apercu, { backgroundColor: theme.colorScheme.surface }]}>{preview}</View>
131
+ </View>
132
+ {selected && (
133
+ <View style={[styles.coche, { backgroundColor: theme.colorScheme.primary, borderColor: theme.colorScheme.surface }]}>
134
+ <MaterialIcons name="check" size={14} color={theme.colorScheme.onPrimary} />
135
+ </View>
136
+ )}
137
+ <Text
138
+ style={[
139
+ theme.textTheme.displayMedium,
140
+ styles.libelle,
141
+ { fontWeight: selected ? '700' : '500' },
142
+ selected ? null : { color: Themer.neural03Color },
143
+ ]}
144
+ >
145
+ {title}
146
+ </Text>
118
147
  </Pressable>
119
148
  );
120
149
  }
@@ -159,6 +188,8 @@ function CustomThemePreview({ theme }: { theme: ThemeData }) {
159
188
  );
160
189
  }
161
190
 
191
+ const ESPACE = 12;
192
+
162
193
  const styles = StyleSheet.create({
163
194
  flex: { flex: 1 },
164
195
  mock: { padding: 10 },
@@ -172,10 +203,12 @@ const styles = StyleSheet.create({
172
203
  dot14: { width: 14, height: 14, borderRadius: 7 },
173
204
  gap12: { height: 12 },
174
205
  gap14: { height: 14 },
175
- wrap: { padding: 20, flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', columnGap: 10, rowGap: 10 },
206
+ contenu: { paddingTop: 8, paddingBottom: 16 },
207
+ card: { margin: 0 },
208
+ grille: { padding: 14, flexDirection: 'row', flexWrap: 'wrap', columnGap: ESPACE, rowGap: 16 },
176
209
  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 },
210
+ cadre: { alignSelf: 'stretch', padding: 3, borderWidth: 2, borderRadius: 14 },
211
+ apercu: { width: '100%', aspectRatio: 0.6, borderRadius: 10, overflow: 'hidden' },
212
+ coche: { position: 'absolute', top: 8, right: 8, borderRadius: 12, borderWidth: 2, padding: 1 },
213
+ libelle: { marginTop: 8, textAlign: 'center' },
181
214
  });
@@ -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} />}
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, backgroundColor: withAlpha(Themer.neural03Color, 0.25) },
71
164
  });