@proulr/sdui-components 0.1.86 → 0.1.88

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 (51) hide show
  1. package/README.md +2 -0
  2. package/package.json +2 -2
  3. package/src/bdc/components/discover.tsx +1 -4
  4. package/src/bdc/components/forms.tsx +4 -0
  5. package/src/bdc/components/inbox.tsx +4 -0
  6. package/src/renderer/SduiScreenRenderer.tsx +14 -2
  7. package/src/renderer/blocks/SduiDiscoverBlocks.tsx +50 -34
  8. package/src/renderer/blocks/SduiInboxBlocks.tsx +26 -11
  9. package/src/renderer/blocks/SduiLayoutBlocks.tsx +39 -46
  10. package/src/renderer/blocks/SduiNativeAdBlock.tsx +7 -8
  11. package/src/renderer/blocks/SduiSocialBlocks.tsx +4 -27
  12. package/src/renderer/lib/discover-profile-formatters.test.ts +7 -0
  13. package/src/renderer/lib/discover-profile-formatters.ts +6 -3
  14. package/src/renderer/sdui-hybrid-renderer-context.tsx +6 -1
  15. package/src/renderer/theme/proulr-theme.test.ts +107 -0
  16. package/src/renderer/theme/proulr-theme.ts +51 -9
  17. package/src/renderer/ui/AppButton.tsx +54 -28
  18. package/src/renderer/ui/AppChromeIconButton.tsx +19 -7
  19. package/src/renderer/ui/AuthStepShell.tsx +1 -2
  20. package/src/renderer/ui/Chip.tsx +41 -34
  21. package/src/renderer/ui/ChipRow.tsx +7 -13
  22. package/src/renderer/ui/HintText.tsx +2 -4
  23. package/src/renderer/ui/InfoBanner.tsx +20 -3
  24. package/src/renderer/ui/Label.tsx +2 -3
  25. package/src/renderer/ui/ListItem.tsx +28 -4
  26. package/src/renderer/ui/RangeSlider.tsx +1 -2
  27. package/src/renderer/ui/SectionHeader.tsx +3 -4
  28. package/src/renderer/ui/SelectRow.tsx +113 -22
  29. package/src/renderer/ui/Skeleton.tsx +13 -4
  30. package/src/renderer/ui/StatRow.tsx +3 -3
  31. package/src/renderer/ui/TextField.tsx +15 -11
  32. package/src/renderer/ui/ToggleRow.tsx +38 -11
  33. package/src/renderer/ui/compose-audience-pill.tsx +2 -3
  34. package/src/renderer/ui/compose-media-carousel.tsx +20 -7
  35. package/src/renderer/ui/compose-media-shortcut.tsx +2 -3
  36. package/src/renderer/ui/compose-pill-input.tsx +28 -9
  37. package/src/renderer/ui/compose-recent-media-grid.tsx +5 -8
  38. package/src/renderer/ui/compose-reply-audience-sheet.tsx +19 -8
  39. package/src/renderer/ui/compose-screen-header.tsx +15 -10
  40. package/src/renderer/ui/compose-text-area.tsx +14 -8
  41. package/src/renderer/ui/compose-toolbar-icon.tsx +9 -5
  42. package/src/renderer/ui/compose-toolbar.tsx +0 -7
  43. package/src/renderer/ui/compose-visibility-row.tsx +2 -3
  44. package/src/renderer/ui/floating-create-menu.tsx +2 -3
  45. package/src/renderer/ui/focus-ring.ts +26 -0
  46. package/src/renderer/ui/index.ts +3 -1
  47. package/src/renderer/ui/story-compose-header.tsx +14 -9
  48. package/src/renderer/ui/story-create-shortcut-row.tsx +1 -1
  49. package/src/renderer/ui/story-editor-chrome.tsx +30 -16
  50. package/src/renderer/ui/story-surface-picker.tsx +1 -2
  51. package/src/renderer/ui/use-reduce-motion.ts +31 -0
@@ -1,16 +1,19 @@
1
1
  import { Pressable, StyleSheet, Text, View } from 'react-native';
2
2
  import { AppIcon, type AppIconName, APP_ICON_SIZE } from './app-icon';
3
- import { colors, fonts, radius, spacing } from '../theme/proulr-theme';
3
+ import { colors, m3Colors, m3Type, radius, spacing } from '../theme/proulr-theme';
4
4
 
5
5
  export function InfoBanner({
6
6
  message,
7
7
  icon,
8
8
  appIcon,
9
+ dismissLabel,
9
10
  onDismiss,
10
11
  }: {
11
12
  message: string;
12
13
  icon?: string;
13
14
  appIcon?: AppIconName;
15
+ /** Obrigatória com `onDismiss`: o ícone isolado não tem nome acessível. */
16
+ dismissLabel?: string;
14
17
  onDismiss?: () => void;
15
18
  }) {
16
19
  const resolvedAppIcon: AppIconName | undefined = appIcon ?? (icon ? undefined : 'info');
@@ -26,7 +29,13 @@ export function InfoBanner({
26
29
  </View>
27
30
  <Text style={styles.infoText}>{message}</Text>
28
31
  {onDismiss ? (
29
- <Pressable onPress={onDismiss} hitSlop={8}>
32
+ <Pressable
33
+ onPress={onDismiss}
34
+ hitSlop={8}
35
+ accessibilityRole="button"
36
+ accessibilityLabel={dismissLabel}
37
+ style={({ pressed }) => [styles.dismissBtn, pressed && styles.dismissBtnPressed]}
38
+ >
30
39
  <AppIcon name="close" size={14} color={colors.textMuted} />
31
40
  </Pressable>
32
41
  ) : null}
@@ -54,5 +63,13 @@ const styles = StyleSheet.create({
54
63
  justifyContent: 'center',
55
64
  },
56
65
  infoIcon: { fontSize: 16, color: colors.accentText },
57
- infoText: { flex: 1, color: colors.textSecondary, fontSize: 13, lineHeight: 18, fontFamily: fonts.medium },
66
+ infoText: { flex: 1, color: colors.textSecondary, ...m3Type.bodySmall },
67
+ dismissBtn: {
68
+ width: 28,
69
+ height: 28,
70
+ borderRadius: 14,
71
+ alignItems: 'center',
72
+ justifyContent: 'center',
73
+ },
74
+ dismissBtnPressed: { backgroundColor: m3Colors.statePressed },
58
75
  });
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { StyleSheet, Text } from 'react-native';
3
- import { colors, fonts, spacing } from '../theme/proulr-theme';
3
+ import { colors, fonts, m3Type, spacing } from '../theme/proulr-theme';
4
4
 
5
5
  export function Label({ children }: { children: ReactNode }) {
6
6
  return <Text style={styles.label}>{children}</Text>;
@@ -8,10 +8,9 @@ export function Label({ children }: { children: ReactNode }) {
8
8
 
9
9
  const styles = StyleSheet.create({
10
10
  label: {
11
+ ...m3Type.labelSmall,
11
12
  color: colors.textSecondary,
12
- fontSize: 12,
13
13
  fontFamily: fonts.bold,
14
- letterSpacing: 0.8,
15
14
  marginTop: spacing.sm,
16
15
  marginBottom: spacing.sm,
17
16
  },
@@ -1,7 +1,8 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { Pressable, StyleSheet, Text, View } from 'react-native';
3
3
  import { AppIcon } from './app-icon';
4
- import { colors, fonts, m3Colors, spacing } from '../theme/proulr-theme';
4
+ import { colors, m3Colors, m3Type, spacing } from '../theme/proulr-theme';
5
+ import { FOCUS_RING_STYLE, useFocusRing } from './focus-ring';
5
6
 
6
7
  export function ListItem({
7
8
  avatar,
@@ -11,6 +12,7 @@ export function ListItem({
11
12
  onPress,
12
13
  showDivider,
13
14
  showChevron,
15
+ accessibilityLabel,
14
16
  }: {
15
17
  avatar?: ReactNode;
16
18
  title: string;
@@ -19,8 +21,10 @@ export function ListItem({
19
21
  onPress?: () => void;
20
22
  showDivider?: boolean;
21
23
  showChevron?: boolean;
24
+ accessibilityLabel?: string;
22
25
  }) {
23
26
  const navChevron = onPress && showChevron !== false && !trailing;
27
+ const focus = useFocusRing();
24
28
 
25
29
  const inner = (
26
30
  <>
@@ -42,7 +46,19 @@ export function ListItem({
42
46
 
43
47
  if (onPress) {
44
48
  return (
45
- <Pressable style={[styles.listItem, showDivider && styles.listItemDivider]} onPress={onPress}>
49
+ <Pressable
50
+ accessibilityRole="button"
51
+ accessibilityLabel={accessibilityLabel ?? title}
52
+ onFocus={focus.onFocus}
53
+ onBlur={focus.onBlur}
54
+ style={({ pressed }) => [
55
+ styles.listItem,
56
+ showDivider && styles.listItemDivider,
57
+ pressed && styles.listItemPressed,
58
+ focus.focused && styles.listItemFocused,
59
+ ]}
60
+ onPress={onPress}
61
+ >
46
62
  {inner}
47
63
  </Pressable>
48
64
  );
@@ -56,6 +72,14 @@ const styles = StyleSheet.create({
56
72
  alignItems: 'center',
57
73
  gap: spacing.md,
58
74
  paddingVertical: spacing.md,
75
+ minHeight: 44,
76
+ },
77
+ listItemPressed: {
78
+ backgroundColor: m3Colors.statePressed,
79
+ },
80
+ listItemFocused: {
81
+ ...FOCUS_RING_STYLE,
82
+ backgroundColor: m3Colors.stateHovered,
59
83
  },
60
84
  listItemDivider: {
61
85
  borderBottomWidth: StyleSheet.hairlineWidth,
@@ -63,7 +87,7 @@ const styles = StyleSheet.create({
63
87
  },
64
88
  listAvatar: {},
65
89
  listContent: { flex: 1 },
66
- listTitle: { color: colors.textPrimary, fontSize: 15, fontFamily: fonts.semibold },
67
- listSubtitle: { color: colors.textSecondary, fontSize: 13, marginTop: 2 },
90
+ listTitle: { ...m3Type.titleSmall, color: colors.textPrimary },
91
+ listSubtitle: { ...m3Type.bodySmall, color: colors.textSecondary, marginTop: 2 },
68
92
  listTrailing: { alignItems: 'flex-end' },
69
93
  });
@@ -168,9 +168,8 @@ const styles = StyleSheet.create({
168
168
  justifyContent: 'space-between',
169
169
  },
170
170
  label: {
171
+ ...m3Type.label,
171
172
  color: m3Colors.onSurface,
172
- fontFamily: m3Type.label.fontFamily,
173
- fontSize: m3Type.label.fontSize,
174
173
  },
175
174
  trackArea: {
176
175
  height: THUMB_SIZE,
@@ -1,5 +1,5 @@
1
1
  import { Pressable, StyleSheet, Text, View } from 'react-native';
2
- import { colors, fonts, spacing } from '../theme/proulr-theme';
2
+ import { colors, fonts, m3Type, spacing } from '../theme/proulr-theme';
3
3
 
4
4
  export function SectionHeader({
5
5
  title,
@@ -31,13 +31,12 @@ const styles = StyleSheet.create({
31
31
  marginTop: spacing.xl,
32
32
  },
33
33
  sectionHeader: {
34
+ ...m3Type.titleSmall,
34
35
  color: colors.textSecondary,
35
- fontSize: 14,
36
- fontFamily: fonts.semibold,
37
36
  },
38
37
  sectionAction: {
38
+ ...m3Type.label,
39
39
  color: colors.accentText,
40
- fontSize: 13,
41
40
  fontFamily: fonts.semibold,
42
41
  },
43
42
  });
@@ -2,7 +2,40 @@ import { useState } from 'react';
2
2
  import { Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
3
3
  import { AppButton } from './AppButton';
4
4
  import { AppIcon } from './app-icon';
5
- import { colors, fonts, radius, spacing } from '../theme/proulr-theme';
5
+ import { FOCUS_RING_STYLE, useFocusRing } from './focus-ring';
6
+ import { colors, fonts, m3Colors, m3Type, radius, spacing } from '../theme/proulr-theme';
7
+
8
+ function OptionRow({
9
+ option,
10
+ selected,
11
+ onSelect,
12
+ }: {
13
+ option: string;
14
+ selected: boolean;
15
+ onSelect: (v: string) => void;
16
+ }) {
17
+ const focus = useFocusRing();
18
+ return (
19
+ <Pressable
20
+ style={({ pressed }) => [
21
+ styles.optionRow,
22
+ pressed && styles.optionRowPressed,
23
+ focus.focused && FOCUS_RING_STYLE,
24
+ ]}
25
+ accessibilityRole="button"
26
+ accessibilityLabel={option}
27
+ accessibilityState={{ selected }}
28
+ onFocus={focus.onFocus}
29
+ onBlur={focus.onBlur}
30
+ onPress={() => onSelect(option)}
31
+ >
32
+ <Text style={{ color: selected ? colors.accentText : colors.white, ...m3Type.body }}>
33
+ {option}
34
+ </Text>
35
+ {selected ? <AppIcon name="check" size={16} color={colors.accent} /> : null}
36
+ </Pressable>
37
+ );
38
+ }
6
39
 
7
40
  function OptionModal({
8
41
  title,
@@ -10,6 +43,8 @@ function OptionModal({
10
43
  options,
11
44
  selected,
12
45
  multi,
46
+ placeholder,
47
+ confirmLabel,
13
48
  onClose,
14
49
  onSelect,
15
50
  }: {
@@ -18,28 +53,33 @@ function OptionModal({
18
53
  options: string[];
19
54
  selected: string[];
20
55
  multi?: boolean;
56
+ placeholder: string;
57
+ confirmLabel: string;
21
58
  onClose: () => void;
22
59
  onSelect: (v: string) => void;
23
60
  }) {
24
61
  return (
25
62
  <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
26
- <Pressable style={styles.modalBackdrop} onPress={onClose}>
63
+ <Pressable
64
+ style={styles.modalBackdrop}
65
+ onPress={onClose}
66
+ accessibilityLabel={placeholder}
67
+ >
27
68
  <Pressable style={styles.modalSheet}>
28
69
  <Text style={styles.modalTitle}>{title}</Text>
29
70
  <ScrollView style={{ maxHeight: 360 }}>
30
- {options.map((opt) => {
31
- const isSel = selected.includes(opt);
32
- return (
33
- <Pressable key={opt} style={styles.optionRow} onPress={() => onSelect(opt)}>
34
- <Text style={{ color: isSel ? colors.accentText : colors.white, fontSize: 16 }}>{opt}</Text>
35
- {isSel ? <AppIcon name="check" size={16} color={colors.accent} /> : null}
36
- </Pressable>
37
- );
38
- })}
71
+ {options.map((opt) => (
72
+ <OptionRow
73
+ key={opt}
74
+ option={opt}
75
+ selected={selected.includes(opt)}
76
+ onSelect={onSelect}
77
+ />
78
+ ))}
39
79
  </ScrollView>
40
80
  {multi ? (
41
81
  <View style={{ marginTop: spacing.md }}>
42
- <AppButton label="Pronto" onPress={onClose} />
82
+ <AppButton label={confirmLabel} onPress={onClose} />
43
83
  </View>
44
84
  ) : null}
45
85
  </Pressable>
@@ -52,20 +92,38 @@ export function SelectRow({
52
92
  label,
53
93
  value,
54
94
  options,
95
+ placeholder,
96
+ confirmLabel = '',
55
97
  onChange,
56
98
  }: {
57
99
  label: string;
58
100
  value?: string | null;
59
101
  options: string[];
102
+ /** Copy vinda do wire/app (Rosetta) — o renderer não traduz. */
103
+ placeholder: string;
104
+ confirmLabel?: string;
60
105
  onChange: (v: string) => void;
61
106
  }) {
62
107
  const [open, setOpen] = useState(false);
108
+ const focus = useFocusRing();
63
109
  return (
64
110
  <>
65
- <Pressable style={styles.selectRow} onPress={() => setOpen(true)}>
111
+ <Pressable
112
+ style={({ pressed }) => [
113
+ styles.selectRow,
114
+ pressed && styles.selectRowPressed,
115
+ focus.focused && FOCUS_RING_STYLE,
116
+ ]}
117
+ accessibilityRole="button"
118
+ accessibilityLabel={label}
119
+ accessibilityHint={placeholder}
120
+ onFocus={focus.onFocus}
121
+ onBlur={focus.onBlur}
122
+ onPress={() => setOpen(true)}
123
+ >
66
124
  <Text style={styles.selectLabel}>{label}</Text>
67
- <Text style={styles.selectValue} numberOfLines={1}>
68
- {value || 'Selecionar'}
125
+ <Text style={[styles.selectValue, !value && styles.selectValueEmpty]} numberOfLines={1}>
126
+ {value || placeholder}
69
127
  </Text>
70
128
  <AppIcon name="chevron-right" size={18} color={colors.textSecondary} />
71
129
  </Pressable>
@@ -74,6 +132,8 @@ export function SelectRow({
74
132
  visible={open}
75
133
  options={options}
76
134
  selected={value ? [value] : []}
135
+ placeholder={placeholder}
136
+ confirmLabel={confirmLabel}
77
137
  onClose={() => setOpen(false)}
78
138
  onSelect={(v) => {
79
139
  onChange(v);
@@ -88,20 +148,37 @@ export function MultiSelectRow({
88
148
  label,
89
149
  values,
90
150
  options,
151
+ placeholder,
152
+ confirmLabel,
91
153
  onChange,
92
154
  }: {
93
155
  label: string;
94
156
  values: string[];
95
157
  options: string[];
158
+ placeholder: string;
159
+ confirmLabel: string;
96
160
  onChange: (v: string[]) => void;
97
161
  }) {
98
162
  const [open, setOpen] = useState(false);
163
+ const focus = useFocusRing();
99
164
  return (
100
165
  <>
101
- <Pressable style={styles.selectRow} onPress={() => setOpen(true)}>
166
+ <Pressable
167
+ style={({ pressed }) => [
168
+ styles.selectRow,
169
+ pressed && styles.selectRowPressed,
170
+ focus.focused && FOCUS_RING_STYLE,
171
+ ]}
172
+ accessibilityRole="button"
173
+ accessibilityLabel={label}
174
+ accessibilityHint={placeholder}
175
+ onFocus={focus.onFocus}
176
+ onBlur={focus.onBlur}
177
+ onPress={() => setOpen(true)}
178
+ >
102
179
  <Text style={styles.selectLabel}>{label}</Text>
103
- <Text style={styles.selectValue} numberOfLines={1}>
104
- {values.length ? values.join(', ') : 'Selecionar'}
180
+ <Text style={[styles.selectValue, !values.length && styles.selectValueEmpty]} numberOfLines={1}>
181
+ {values.length ? values.join(', ') : placeholder}
105
182
  </Text>
106
183
  <AppIcon name="chevron-right" size={18} color={colors.textSecondary} />
107
184
  </Pressable>
@@ -111,6 +188,8 @@ export function MultiSelectRow({
111
188
  options={options}
112
189
  selected={values}
113
190
  multi
191
+ placeholder={placeholder}
192
+ confirmLabel={confirmLabel}
114
193
  onClose={() => setOpen(false)}
115
194
  onSelect={(v) => {
116
195
  const set = new Set(values);
@@ -128,13 +207,18 @@ const styles = StyleSheet.create({
128
207
  flexDirection: 'row',
129
208
  alignItems: 'center',
130
209
  gap: spacing.sm,
210
+ minHeight: 48,
131
211
  paddingHorizontal: spacing.sm,
132
212
  paddingVertical: 15,
133
213
  borderRadius: radius.lg,
134
214
  backgroundColor: colors.surface2,
215
+ borderWidth: 1,
216
+ borderColor: m3Colors.outlineInteractive,
135
217
  },
136
- selectLabel: { flex: 1, color: colors.white, fontSize: 15, fontFamily: fonts.medium },
137
- selectValue: { color: colors.textSecondary, fontSize: 14, maxWidth: 160 },
218
+ selectRowPressed: { backgroundColor: m3Colors.surfaceContainerHighest },
219
+ selectLabel: { flex: 1, color: colors.white, ...m3Type.body, fontFamily: fonts.medium },
220
+ selectValue: { ...m3Type.bodySmall, color: colors.textSecondary, maxWidth: 160 },
221
+ selectValueEmpty: { color: colors.textMuted },
138
222
  modalBackdrop: { flex: 1, backgroundColor: colors.overlay, justifyContent: 'flex-end' },
139
223
  modalSheet: {
140
224
  backgroundColor: colors.surface,
@@ -144,13 +228,20 @@ const styles = StyleSheet.create({
144
228
  borderTopWidth: 1,
145
229
  borderColor: colors.border,
146
230
  },
147
- modalTitle: { color: colors.white, fontSize: 18, fontFamily: fonts.bold, marginBottom: spacing.md },
231
+ modalTitle: {
232
+ color: colors.white,
233
+ marginBottom: spacing.md,
234
+ ...m3Type.title,
235
+ fontFamily: fonts.bold,
236
+ },
148
237
  optionRow: {
149
238
  flexDirection: 'row',
150
239
  justifyContent: 'space-between',
151
240
  alignItems: 'center',
241
+ minHeight: 48,
152
242
  paddingVertical: 14,
153
243
  borderBottomWidth: StyleSheet.hairlineWidth,
154
- borderBottomColor: colors.border,
244
+ borderBottomColor: m3Colors.divider,
155
245
  },
246
+ optionRowPressed: { backgroundColor: m3Colors.surfaceContainerHigh },
156
247
  });
@@ -9,19 +9,28 @@ import Animated, {
9
9
  withTiming,
10
10
  } from 'react-native-reanimated';
11
11
  import { m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
12
+ import { useReduceMotion } from './use-reduce-motion';
12
13
 
13
14
  const PULSE_DURATION_MS = 900;
14
15
  const PULSE_MIN_OPACITY = 0.45;
16
+ const PULSE_STATIC_OPACITY = 0.7;
15
17
 
16
18
  /**
17
19
  * Pulso de opacidade compartilhado por cada bloco de skeleton. Respeita o
18
- * "reduzir movimento" do sistema (fica estatico). Como `react-native-reanimated`
19
- * ja e usado nos primitivos do pacote (ex.: RangeSlider), importamos direto.
20
+ * "reduzir movimento" do sistema: o loop infinito é desligado por completo
21
+ * (com `ReduceMotion.System` puro, o `withRepeat` alternaria 0↠1 a cada
22
+ * frame, virando strobe). Como `react-native-reanimated` ja e usado nos
23
+ * primitivos do pacote (ex.: RangeSlider), importamos direto.
20
24
  */
21
25
  function usePulseStyle() {
22
- const progress = useSharedValue(PULSE_MIN_OPACITY);
26
+ const reducedMotion = useReduceMotion();
27
+ const progress = useSharedValue(reducedMotion ? PULSE_STATIC_OPACITY : PULSE_MIN_OPACITY);
23
28
 
24
29
  useEffect(() => {
30
+ if (reducedMotion) {
31
+ progress.value = PULSE_STATIC_OPACITY;
32
+ return;
33
+ }
25
34
  progress.value = withRepeat(
26
35
  withTiming(1, {
27
36
  duration: PULSE_DURATION_MS,
@@ -31,7 +40,7 @@ function usePulseStyle() {
31
40
  -1,
32
41
  true,
33
42
  );
34
- }, [progress]);
43
+ }, [progress, reducedMotion]);
35
44
 
36
45
  return useAnimatedStyle(() => ({ opacity: progress.value }));
37
46
  }
@@ -1,6 +1,6 @@
1
1
  import { Fragment } from 'react';
2
2
  import { StyleSheet, Text, View } from 'react-native';
3
- import { colors, fonts, radius, spacing } from '../theme/proulr-theme';
3
+ import { colors, fonts, m3Type, radius, spacing } from '../theme/proulr-theme';
4
4
 
5
5
  export function StatRow({ stats }: { stats: { value: string; label: string; highlight?: boolean }[] }) {
6
6
  return (
@@ -28,6 +28,6 @@ const styles = StyleSheet.create({
28
28
  },
29
29
  statItem: { flex: 1, alignItems: 'center' },
30
30
  statDivider: { width: 1, height: 36, backgroundColor: colors.border },
31
- statValue: { color: colors.white, fontSize: 18, fontFamily: fonts.bold },
32
- statLabel: { color: colors.textSecondary, fontSize: 12, marginTop: 2, fontFamily: fonts.medium },
31
+ statValue: { ...m3Type.body, color: colors.white, fontFamily: fonts.bold },
32
+ statLabel: { ...m3Type.labelSmall, color: colors.textSecondary, marginTop: 2 },
33
33
  });
@@ -10,7 +10,7 @@ import {
10
10
  ViewStyle,
11
11
  } from 'react-native';
12
12
  import { AppIcon, type AppIconName } from './app-icon';
13
- import { fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
13
+ import { m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
14
14
 
15
15
  const webInputLock =
16
16
  Platform.OS === 'web'
@@ -33,7 +33,7 @@ export function TextField(
33
33
  style={[
34
34
  styles.field,
35
35
  multiline && styles.fieldMultiline,
36
- focused && { borderColor: m3Colors.primary, borderWidth: 2 },
36
+ focused && { borderColor: m3Colors.focus },
37
37
  containerStyle,
38
38
  ]}
39
39
  >
@@ -53,9 +53,12 @@ export function PasswordField(
53
53
  props: TextInputProps & {
54
54
  containerStyle?: StyleProp<ViewStyle>;
55
55
  inputRef?: React.Ref<TextInput>;
56
+ /** Copy de acessibilidade vem do app/BFF (Rosetta) — o renderer não traduz. */
57
+ hideLabel: string;
58
+ showLabel: string;
56
59
  },
57
60
  ) {
58
- const { containerStyle, inputRef, ...rest } = props;
61
+ const { containerStyle, inputRef, hideLabel, showLabel, ...rest } = props;
59
62
  const [visible, setVisible] = useState(false);
60
63
 
61
64
  return (
@@ -70,8 +73,8 @@ export function PasswordField(
70
73
  <Pressable
71
74
  onPress={() => setVisible((v) => !v)}
72
75
  hitSlop={8}
73
- style={styles.passwordToggle}
74
- accessibilityLabel={visible ? 'Ocultar senha' : 'Mostrar senha'}
76
+ style={({ pressed }) => [styles.passwordToggle, pressed && styles.passwordTogglePressed]}
77
+ accessibilityLabel={visible ? hideLabel : showLabel}
75
78
  accessibilityRole="button"
76
79
  >
77
80
  <AppIcon
@@ -90,7 +93,7 @@ const styles = StyleSheet.create({
90
93
  borderRadius: m3Shape.cornerExtraSmall,
91
94
  backgroundColor: 'transparent',
92
95
  borderWidth: 1,
93
- borderColor: m3Colors.outline,
96
+ borderColor: m3Colors.outlineInteractive,
94
97
  paddingHorizontal: spacing.sm,
95
98
  justifyContent: 'center',
96
99
  },
@@ -99,19 +102,20 @@ const styles = StyleSheet.create({
99
102
  justifyContent: 'flex-start',
100
103
  paddingVertical: spacing.md,
101
104
  },
102
- fieldInput: { color: m3Colors.onSurface, fontSize: 16, fontFamily: fonts.regular },
105
+ fieldInput: { ...m3Type.body, color: m3Colors.onSurface },
103
106
  fieldInputMultiline: {
104
- fontSize: 18,
105
- lineHeight: 26,
107
+ ...m3Type.body,
106
108
  minHeight: 96,
107
109
  width: '100%',
108
110
  },
109
111
  passwordField: { flexDirection: 'row', alignItems: 'center', paddingRight: spacing.sm },
110
112
  passwordInput: { flex: 1 },
111
113
  passwordToggle: {
112
- width: 40,
113
- height: 40,
114
+ width: 44,
115
+ height: 44,
114
116
  alignItems: 'center',
115
117
  justifyContent: 'center',
118
+ borderRadius: m3Shape.cornerSmall,
116
119
  },
120
+ passwordTogglePressed: { backgroundColor: m3Colors.surfaceContainerHigh },
117
121
  });
@@ -1,5 +1,6 @@
1
- import { StyleSheet, Switch, Text, View } from 'react-native';
2
- import { fonts, m3Colors, spacing } from '../theme/proulr-theme';
1
+ import { Pressable, StyleSheet, Switch, Text, View } from 'react-native';
2
+ import { fonts, m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
3
+ import { FOCUS_RING_STYLE, useFocusRing } from './focus-ring';
3
4
 
4
5
  export function ToggleRow({
5
6
  label,
@@ -7,27 +8,49 @@ export function ToggleRow({
7
8
  value,
8
9
  onChange,
9
10
  disabled,
11
+ testID,
10
12
  }: {
11
13
  label: string;
12
14
  hint?: string;
13
15
  value: boolean;
14
16
  onChange: (value: boolean) => void;
15
17
  disabled?: boolean;
18
+ testID?: string;
16
19
  }) {
20
+ const focus = useFocusRing();
17
21
  return (
18
- <View style={styles.row}>
22
+ <Pressable
23
+ testID={testID}
24
+ accessibilityRole="switch"
25
+ accessibilityLabel={label}
26
+ accessibilityState={{ checked: value, disabled: Boolean(disabled) }}
27
+ accessibilityHint={hint}
28
+ disabled={disabled}
29
+ onPress={() => onChange(!value)}
30
+ onFocus={focus.onFocus}
31
+ onBlur={focus.onBlur}
32
+ style={({ pressed }) => [
33
+ styles.row,
34
+ pressed && !disabled && styles.rowPressed,
35
+ focus.focused && !disabled && FOCUS_RING_STYLE,
36
+ ]}
37
+ >
19
38
  <View style={styles.copy}>
20
- <Text style={styles.label}>{label}</Text>
39
+ <Text style={[styles.label, disabled && styles.labelDisabled]}>{label}</Text>
21
40
  {hint ? <Text style={styles.hint}>{hint}</Text> : null}
22
41
  </View>
23
42
  <Switch
24
43
  value={value}
25
44
  onValueChange={onChange}
26
45
  disabled={disabled}
27
- trackColor={{ false: m3Colors.surfaceContainerHighest, true: m3Colors.primary }}
28
- thumbColor={m3Colors.onPrimary}
46
+ accessibilityElementsHidden
47
+ trackColor={{
48
+ false: m3Colors.surfaceContainerHighest,
49
+ true: disabled ? m3Colors.disabledContainer : m3Colors.primary,
50
+ }}
51
+ thumbColor={value ? m3Colors.onPrimary : m3Colors.onSurfaceVariant}
29
52
  />
30
- </View>
53
+ </Pressable>
31
54
  );
32
55
  }
33
56
 
@@ -36,21 +59,25 @@ const styles = StyleSheet.create({
36
59
  flexDirection: 'row',
37
60
  alignItems: 'center',
38
61
  gap: spacing.md,
39
- paddingVertical: spacing.xs,
62
+ minHeight: 48,
63
+ paddingVertical: spacing.sm,
64
+ paddingHorizontal: spacing.sm,
65
+ borderRadius: m3Shape.cornerMedium,
40
66
  },
67
+ rowPressed: { backgroundColor: m3Colors.surfaceContainerHigh },
41
68
  copy: {
42
69
  flex: 1,
43
70
  gap: spacing.xs,
44
71
  },
45
72
  label: {
46
73
  color: m3Colors.onSurface,
47
- fontSize: 15,
74
+ ...m3Type.body,
48
75
  fontFamily: fonts.semibold,
49
76
  },
77
+ labelDisabled: { color: m3Colors.onDisabled },
50
78
  hint: {
51
79
  color: m3Colors.onSurfaceVariant,
52
- fontSize: 12,
53
- lineHeight: 17,
80
+ ...m3Type.labelSmall,
54
81
  fontFamily: fonts.regular,
55
82
  },
56
83
  });
@@ -1,6 +1,6 @@
1
1
  import { Pressable, StyleSheet, Text } from 'react-native';
2
2
  import { AppIcon } from './app-icon';
3
- import { fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
3
+ import { m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
4
4
 
5
5
  export function ComposeAudiencePill({
6
6
  label,
@@ -36,8 +36,7 @@ const styles = StyleSheet.create({
36
36
  backgroundColor: m3Colors.primaryContainer,
37
37
  },
38
38
  label: {
39
+ ...m3Type.label,
39
40
  color: m3Colors.onPrimaryContainer,
40
- fontFamily: fonts.medium,
41
- fontSize: 13,
42
41
  },
43
42
  });