@proulr/sdui-components 0.1.85 → 0.1.87

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 (48) hide show
  1. package/package.json +1 -1
  2. package/src/bdc/components/discover.tsx +1 -4
  3. package/src/bdc/components/forms.tsx +4 -0
  4. package/src/bdc/components/inbox.tsx +4 -0
  5. package/src/renderer/SduiScreenRenderer.tsx +14 -2
  6. package/src/renderer/blocks/SduiDiscoverBlocks.tsx +50 -34
  7. package/src/renderer/blocks/SduiInboxBlocks.tsx +26 -11
  8. package/src/renderer/blocks/SduiLayoutBlocks.tsx +39 -46
  9. package/src/renderer/blocks/SduiNativeAdBlock.tsx +7 -8
  10. package/src/renderer/blocks/SduiSocialBlocks.tsx +4 -27
  11. package/src/renderer/lib/discover-profile-formatters.test.ts +7 -0
  12. package/src/renderer/lib/discover-profile-formatters.ts +6 -3
  13. package/src/renderer/sdui-hybrid-renderer-context.tsx +6 -1
  14. package/src/renderer/theme/proulr-theme.test.ts +107 -0
  15. package/src/renderer/theme/proulr-theme.ts +51 -9
  16. package/src/renderer/ui/AppButton.tsx +54 -28
  17. package/src/renderer/ui/AppChromeIconButton.tsx +19 -7
  18. package/src/renderer/ui/AuthStepShell.tsx +1 -2
  19. package/src/renderer/ui/Chip.tsx +41 -34
  20. package/src/renderer/ui/ChipRow.tsx +7 -13
  21. package/src/renderer/ui/HintText.tsx +2 -4
  22. package/src/renderer/ui/InfoBanner.tsx +20 -3
  23. package/src/renderer/ui/Label.tsx +2 -3
  24. package/src/renderer/ui/ListItem.tsx +28 -4
  25. package/src/renderer/ui/RangeSlider.tsx +1 -2
  26. package/src/renderer/ui/SectionHeader.tsx +3 -4
  27. package/src/renderer/ui/SelectRow.tsx +113 -22
  28. package/src/renderer/ui/StatRow.tsx +3 -3
  29. package/src/renderer/ui/TextField.tsx +15 -11
  30. package/src/renderer/ui/ToggleRow.tsx +38 -11
  31. package/src/renderer/ui/compose-audience-pill.tsx +2 -3
  32. package/src/renderer/ui/compose-media-carousel.tsx +20 -7
  33. package/src/renderer/ui/compose-media-shortcut.tsx +2 -3
  34. package/src/renderer/ui/compose-pill-input.tsx +28 -9
  35. package/src/renderer/ui/compose-recent-media-grid.tsx +5 -8
  36. package/src/renderer/ui/compose-reply-audience-sheet.tsx +19 -8
  37. package/src/renderer/ui/compose-screen-header.tsx +15 -10
  38. package/src/renderer/ui/compose-text-area.tsx +14 -8
  39. package/src/renderer/ui/compose-toolbar-icon.tsx +9 -5
  40. package/src/renderer/ui/compose-toolbar.tsx +0 -7
  41. package/src/renderer/ui/compose-visibility-row.tsx +2 -3
  42. package/src/renderer/ui/floating-create-menu.tsx +2 -3
  43. package/src/renderer/ui/focus-ring.ts +26 -0
  44. package/src/renderer/ui/index.ts +2 -1
  45. package/src/renderer/ui/story-compose-header.tsx +14 -9
  46. package/src/renderer/ui/story-create-shortcut-row.tsx +1 -1
  47. package/src/renderer/ui/story-editor-chrome.tsx +30 -16
  48. package/src/renderer/ui/story-surface-picker.tsx +1 -2
@@ -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
  });
@@ -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
  });
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { Image, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
3
3
  import { AppIcon } from './app-icon';
4
- import { fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
4
+ import { fonts, m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
5
5
 
6
6
  export type ComposeMediaCarouselItem = {
7
7
  id: string;
@@ -11,14 +11,25 @@ export type ComposeMediaCarouselItem = {
11
11
 
12
12
  const TILE_SIZE = 168;
13
13
 
14
+ /** Coluna do compositor: 40 de avatar + 8 de gap — o rail de mídia alinha com o texto. */
15
+ const COMPOSER_TEXT_GUTTER = 48;
16
+
17
+ /** O badge visual é pequeno para não cobrir a miniatura; o alvo de toque cresce com o slop. */
18
+ const REMOVE_HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 };
19
+
14
20
  export function ComposeMediaCarousel({
15
21
  items,
16
22
  limit,
23
+ videoLabel,
24
+ removeLabel,
17
25
  onRemove,
18
26
  renderVideo,
19
27
  }: {
20
28
  items: ComposeMediaCarouselItem[];
21
29
  limit: number;
30
+ /** Copy vem do caller (Rosetta) — o renderer não traduz. */
31
+ videoLabel: string;
32
+ removeLabel: string;
22
33
  onRemove: (id: string) => void;
23
34
  renderVideo?: (uri: string) => ReactNode;
24
35
  }) {
@@ -38,7 +49,7 @@ export function ComposeMediaCarousel({
38
49
  renderVideo(item.uri)
39
50
  ) : (
40
51
  <View style={styles.videoFallback}>
41
- <Text style={styles.videoFallbackText}>Vídeo</Text>
52
+ <Text style={styles.videoFallbackText}>{videoLabel}</Text>
42
53
  </View>
43
54
  )
44
55
  ) : (
@@ -47,8 +58,9 @@ export function ComposeMediaCarousel({
47
58
  <Pressable
48
59
  onPress={() => onRemove(item.id)}
49
60
  accessibilityRole="button"
50
- accessibilityLabel="Remover mídia"
51
- style={styles.removeBtn}
61
+ accessibilityLabel={removeLabel}
62
+ hitSlop={REMOVE_HIT_SLOP}
63
+ style={({ pressed }) => [styles.removeBtn, pressed && styles.removeBtnPressed]}
52
64
  >
53
65
  <AppIcon name="close" size={14} color={m3Colors.onPrimary} />
54
66
  </Pressable>
@@ -65,7 +77,7 @@ export function ComposeMediaCarousel({
65
77
  const styles = StyleSheet.create({
66
78
  wrap: {
67
79
  marginTop: spacing.md,
68
- marginLeft: 48,
80
+ marginLeft: COMPOSER_TEXT_GUTTER,
69
81
  },
70
82
  scrollContent: {
71
83
  gap: spacing.sm,
@@ -89,8 +101,8 @@ const styles = StyleSheet.create({
89
101
  },
90
102
  videoFallbackText: {
91
103
  color: m3Colors.onSurfaceVariant,
104
+ ...m3Type.bodySmall,
92
105
  fontFamily: fonts.medium,
93
- fontSize: 13,
94
106
  },
95
107
  removeBtn: {
96
108
  position: 'absolute',
@@ -103,10 +115,11 @@ const styles = StyleSheet.create({
103
115
  alignItems: 'center',
104
116
  justifyContent: 'center',
105
117
  },
118
+ removeBtnPressed: { backgroundColor: m3Colors.statePressed },
106
119
  counter: {
107
120
  marginTop: spacing.xs,
108
121
  color: m3Colors.onSurfaceVariant,
122
+ ...m3Type.labelSmall,
109
123
  fontFamily: fonts.medium,
110
- fontSize: 12,
111
124
  },
112
125
  });
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { Pressable, StyleSheet, Text, View } from 'react-native';
3
- import { fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
3
+ import { m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
4
4
 
5
5
  const MEDIA_TILE_SIZE = 88;
6
6
 
@@ -48,9 +48,8 @@ const styles = StyleSheet.create({
48
48
  position: 'relative',
49
49
  },
50
50
  hintText: {
51
+ ...m3Type.labelSmall,
51
52
  color: m3Colors.onPrimary,
52
- fontFamily: fonts.medium,
53
- fontSize: 11,
54
53
  },
55
54
  hintTail: {
56
55
  position: 'absolute',