@proulr/sdui-components 0.1.86 → 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
@@ -18,7 +18,7 @@ import {
18
18
  } from 'react-native';
19
19
  import { AppIcon } from './app-icon';
20
20
  import { ChromeGlassLayer } from './ChromeGlass';
21
- import { colors, fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
21
+ import { colors, fonts, m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
22
22
 
23
23
  if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
24
24
  UIManager.setLayoutAnimationEnabledExperimental(true);
@@ -27,8 +27,12 @@ if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental
27
27
  const ACTION_BUTTON_SIZE = 48;
28
28
  /** Filled action circle when nested inside the pill (Instagram-style composer). */
29
29
  const ACTION_BUTTON_SIZE_INSET = 36;
30
- const FONT_SIZE = 16;
31
- const LINE_HEIGHT = 25;
30
+ /** O círculo inset é menor visualmente; o slop devolve os 44pt de alvo de toque. */
31
+ const ACTION_BUTTON_INSET_HIT_SLOP = { top: 4, bottom: 4, left: 4, right: 4 };
32
+ // A medição de altura do composer depende da linha do texto: deriva do token de
33
+ // corpo para não criar uma segunda escala tipográfica no design system.
34
+ const FONT_SIZE = m3Type.body.fontSize;
35
+ const LINE_HEIGHT = m3Type.body.lineHeight;
32
36
  const MAX_LINES_DEFAULT = 4;
33
37
  const PILL_MIN_HEIGHT = ACTION_BUTTON_SIZE;
34
38
  const VERTICAL_PAD = (PILL_MIN_HEIGHT - LINE_HEIGHT) / 2;
@@ -38,6 +42,9 @@ export type ComposePillVariant = 'surface' | 'chrome' | 'overlay';
38
42
  export type ComposePillReplyPreview = {
39
43
  title: string;
40
44
  body: string;
45
+ /** Rótulo de acessibilidade do botão de dispensa — vem de quem compõe o wire,
46
+ * o renderer não traduz copy. */
47
+ dismissLabel: string;
41
48
  onDismiss: () => void;
42
49
  };
43
50
 
@@ -340,7 +347,7 @@ export function ComposePillReplyBar({
340
347
  hitSlop={8}
341
348
  style={styles.replyBarClose}
342
349
  accessibilityRole="button"
343
- accessibilityLabel="Cancelar resposta"
350
+ accessibilityLabel={preview.dismissLabel}
344
351
  >
345
352
  <AppIcon name="close" size={14} color={m3Colors.onSurfaceVariant} />
346
353
  </Pressable>
@@ -361,7 +368,11 @@ export function ComposePillIconButton({
361
368
  }) {
362
369
  return (
363
370
  <Pressable
364
- style={[styles.iconButton, disabled && styles.iconButtonDisabled]}
371
+ style={({ pressed }) => [
372
+ styles.iconButton,
373
+ disabled && styles.iconButtonDisabled,
374
+ pressed && !disabled && styles.iconButtonPressed,
375
+ ]}
365
376
  onPress={onPress}
366
377
  disabled={disabled || !onPress}
367
378
  hitSlop={6}
@@ -390,15 +401,17 @@ export function ComposePillActionButton({
390
401
  const size = inset ? ACTION_BUTTON_SIZE_INSET : ACTION_BUTTON_SIZE;
391
402
  return (
392
403
  <Pressable
393
- style={[
404
+ style={({ pressed }) => [
394
405
  styles.actionButton,
395
406
  { width: size, height: size, borderRadius: size / 2 },
396
407
  disabled && styles.actionButtonDisabled,
408
+ pressed && !disabled && styles.actionButtonPressed,
397
409
  ]}
398
410
  onPress={onPress}
399
411
  disabled={disabled || !onPress}
400
412
  accessibilityRole="button"
401
413
  accessibilityLabel={label}
414
+ hitSlop={inset ? ACTION_BUTTON_INSET_HIT_SLOP : undefined}
402
415
  >
403
416
  {children}
404
417
  </Pressable>
@@ -571,17 +584,17 @@ const styles = StyleSheet.create({
571
584
  },
572
585
  replyBarContent: {
573
586
  flex: 1,
574
- gap: 2,
587
+ gap: spacing.xs,
575
588
  zIndex: 1,
576
589
  },
577
590
  replyBarName: {
591
+ ...m3Type.labelSmall,
578
592
  color: m3Colors.primary,
579
- fontSize: 13,
580
593
  fontFamily: fonts.semibold,
581
594
  },
582
595
  replyBarPreview: {
596
+ ...m3Type.labelSmall,
583
597
  color: m3Colors.onSurfaceVariant,
584
- fontSize: 13,
585
598
  fontFamily: fonts.regular,
586
599
  },
587
600
  replyBarClose: {
@@ -632,6 +645,9 @@ const styles = StyleSheet.create({
632
645
  iconButtonDisabled: {
633
646
  opacity: 0.4,
634
647
  },
648
+ iconButtonPressed: {
649
+ opacity: 0.85,
650
+ },
635
651
  trailingIcons: {
636
652
  flexDirection: 'row',
637
653
  alignItems: 'center',
@@ -688,6 +704,9 @@ const styles = StyleSheet.create({
688
704
  actionButtonDisabled: {
689
705
  opacity: 0.45,
690
706
  },
707
+ actionButtonPressed: {
708
+ opacity: 0.85,
709
+ },
691
710
  });
692
711
 
693
712
  export const COMPOSE_PILL_ACTION_BUTTON_SIZE = ACTION_BUTTON_SIZE;
@@ -7,7 +7,7 @@ import {
7
7
  useWindowDimensions,
8
8
  View,
9
9
  } from 'react-native';
10
- import { fonts, m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
10
+ import { m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
11
11
  import { AppIcon } from './app-icon';
12
12
  import { AppLoadingSpinner } from './AppLoadingSpinner';
13
13
  import { StoryTextGlyph } from './story-glyphs';
@@ -210,9 +210,8 @@ const styles = StyleSheet.create({
210
210
  borderColor: m3Colors.outlineVariant,
211
211
  },
212
212
  selectLabel: {
213
+ ...m3Type.label,
213
214
  color: m3Colors.onSurface,
214
- fontFamily: fonts.medium,
215
- fontSize: 13,
216
215
  },
217
216
  loadingWrap: {
218
217
  alignItems: 'center',
@@ -255,14 +254,12 @@ const styles = StyleSheet.create({
255
254
  backgroundColor: m3Colors.scrim,
256
255
  },
257
256
  videoBadgeText: {
257
+ ...m3Type.labelSmall,
258
258
  color: m3Colors.onPrimary,
259
- fontFamily: fonts.medium,
260
- fontSize: 10,
261
259
  },
262
260
  textCellLabel: {
263
- marginTop: spacing.xs,
261
+ ...m3Type.labelSmall,
264
262
  color: m3Colors.onSurfaceVariant,
265
- fontFamily: fonts.medium,
266
- fontSize: 11,
263
+ marginTop: spacing.xs,
267
264
  },
268
265
  });
@@ -14,12 +14,19 @@ export type ComposeReplyAudienceOption = {
14
14
 
15
15
  export function ComposeReplyAudienceSheet({
16
16
  visible,
17
+ title,
18
+ subtitle,
19
+ closeLabel,
17
20
  value,
18
21
  options,
19
22
  onClose,
20
23
  onSelect,
21
24
  }: {
22
25
  visible: boolean;
26
+ /** Copy do caller (Rosetta) — o renderer não traduz. */
27
+ title: string;
28
+ subtitle: string;
29
+ closeLabel: string;
23
30
  value: ComposeReplyAudienceValue;
24
31
  options: ComposeReplyAudienceOption[];
25
32
  onClose: () => void;
@@ -28,13 +35,16 @@ export function ComposeReplyAudienceSheet({
28
35
  return (
29
36
  <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
30
37
  <View style={styles.backdrop}>
31
- <Pressable style={styles.backdropPress} onPress={onClose} accessibilityLabel="Fechar" />
38
+ <Pressable
39
+ style={styles.backdropPress}
40
+ onPress={onClose}
41
+ accessibilityRole="button"
42
+ accessibilityLabel={closeLabel}
43
+ />
32
44
  <View style={styles.sheet}>
33
45
  <View style={styles.handle} />
34
- <Text style={styles.title}>Quem pode responder?</Text>
35
- <Text style={styles.subtitle}>
36
- Escolha quem pode responder a este post. Perfis mencionados sempre poderão responder.
37
- </Text>
46
+ <Text style={styles.title}>{title}</Text>
47
+ <Text style={styles.subtitle}>{subtitle}</Text>
38
48
 
39
49
  {options.map((option) => {
40
50
  const selected = option.value === value;
@@ -47,7 +57,7 @@ export function ComposeReplyAudienceSheet({
47
57
  }}
48
58
  accessibilityRole="button"
49
59
  accessibilityState={{ selected }}
50
- style={({ pressed }) => [styles.option, pressed && { opacity: 0.86 }]}
60
+ style={({ pressed }) => [styles.option, pressed && styles.optionPressed]}
51
61
  >
52
62
  <View style={styles.optionIconWrap}>{option.icon}</View>
53
63
  <View style={styles.optionBody}>
@@ -107,14 +117,15 @@ const styles = StyleSheet.create({
107
117
  color: m3Colors.onSurfaceVariant,
108
118
  ...m3Type.bodySmall,
109
119
  marginBottom: spacing.md,
110
- lineHeight: 18,
111
120
  },
112
121
  option: {
113
122
  flexDirection: 'row',
114
123
  alignItems: 'center',
124
+ minHeight: 56,
115
125
  paddingVertical: spacing.md,
116
126
  gap: spacing.md,
117
127
  },
128
+ optionPressed: { backgroundColor: m3Colors.statePressed },
118
129
  optionIconWrap: {
119
130
  width: 44,
120
131
  height: 44,
@@ -129,8 +140,8 @@ const styles = StyleSheet.create({
129
140
  },
130
141
  optionLabel: {
131
142
  color: m3Colors.onSurface,
143
+ ...m3Type.titleSmall,
132
144
  fontFamily: fonts.semibold,
133
- fontSize: 16,
134
145
  },
135
146
  optionHint: {
136
147
  color: m3Colors.onSurfaceVariant,
@@ -6,14 +6,17 @@ import { AppLoadingSpinner } from './AppLoadingSpinner';
6
6
 
7
7
  export function ComposeScreenHeader({
8
8
  title,
9
- actionLabel = 'Publicar',
9
+ closeLabel,
10
+ actionLabel,
10
11
  canAction,
11
12
  loading,
12
13
  onClose,
13
14
  onAction,
14
15
  }: {
15
16
  title?: string;
16
- actionLabel?: string;
17
+ /** Copy vem do caller (wire/Rosetta) — o renderer não traduz nem tem default de produto. */
18
+ closeLabel: string;
19
+ actionLabel: string;
17
20
  canAction: boolean;
18
21
  loading?: boolean;
19
22
  onClose: () => void;
@@ -25,10 +28,9 @@ export function ComposeScreenHeader({
25
28
  <View style={[styles.header, { paddingTop: insets.top + spacing.xs }]}>
26
29
  <Pressable
27
30
  onPress={onClose}
28
- hitSlop={10}
29
31
  accessibilityRole="button"
30
- accessibilityLabel="Fechar"
31
- style={styles.closeBtn}
32
+ accessibilityLabel={closeLabel}
33
+ style={({ pressed }) => [styles.closeBtn, pressed && styles.closeBtnPressed]}
32
34
  >
33
35
  <AppIcon name="close" color={m3Colors.onSurface} size={18} />
34
36
  </Pressable>
@@ -50,7 +52,7 @@ export function ComposeScreenHeader({
50
52
  styles.publishPill,
51
53
  canAction && styles.publishPillActive,
52
54
  (!canAction || loading) && styles.publishPillDisabled,
53
- pressed && canAction && { opacity: 0.88 },
55
+ pressed && canAction && styles.publishPillPressed,
54
56
  ]}
55
57
  >
56
58
  {loading ? (
@@ -75,11 +77,13 @@ const styles = StyleSheet.create({
75
77
  backgroundColor: m3Colors.surface,
76
78
  },
77
79
  closeBtn: {
78
- width: 40,
79
- height: 40,
80
+ width: 44,
81
+ height: 44,
80
82
  alignItems: 'center',
81
83
  justifyContent: 'center',
84
+ borderRadius: m3Shape.cornerFull,
82
85
  },
86
+ closeBtnPressed: { backgroundColor: m3Colors.statePressed },
83
87
  title: {
84
88
  flex: 1,
85
89
  color: m3Colors.onSurface,
@@ -91,7 +95,7 @@ const styles = StyleSheet.create({
91
95
  },
92
96
  publishPill: {
93
97
  minWidth: 88,
94
- height: 36,
98
+ height: 44,
95
99
  borderRadius: m3Shape.cornerFull,
96
100
  alignItems: 'center',
97
101
  justifyContent: 'center',
@@ -101,13 +105,14 @@ const styles = StyleSheet.create({
101
105
  publishPillActive: {
102
106
  backgroundColor: m3Colors.primary,
103
107
  },
108
+ publishPillPressed: { opacity: 0.88 },
104
109
  publishPillDisabled: {
105
110
  opacity: 0.45,
106
111
  },
107
112
  publishLabel: {
108
113
  color: m3Colors.onSurfaceVariant,
114
+ ...m3Type.label,
109
115
  fontFamily: fonts.semibold,
110
- fontSize: 14,
111
116
  },
112
117
  publishLabelActive: {
113
118
  color: m3Colors.onPrimary,
@@ -1,11 +1,14 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { Platform, Pressable, StyleSheet, TextInput, TextInputProps, 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
- export function ComposeTextArea(props: TextInputProps) {
5
+ export function ComposeTextArea({
6
+ placeholder,
7
+ ...props
8
+ }: TextInputProps & { placeholder: string }) {
6
9
  return (
7
10
  <TextInput
8
- placeholder="O que está acontecendo?"
11
+ placeholder={placeholder}
9
12
  placeholderTextColor={m3Colors.onSurfaceVariant}
10
13
  multiline
11
14
  style={[styles.input, Platform.OS === 'web' ? styles.inputWeb : null]}
@@ -17,9 +20,12 @@ export function ComposeTextArea(props: TextInputProps) {
17
20
 
18
21
  export function ComposeMediaPreviewFrame({
19
22
  children,
23
+ removeLabel,
20
24
  onRemove,
21
25
  }: {
22
26
  children: ReactNode;
27
+ /** Copy do caller (Rosetta) — o renderer não traduz. */
28
+ removeLabel: string;
23
29
  onRemove: () => void;
24
30
  }) {
25
31
  return (
@@ -28,8 +34,9 @@ export function ComposeMediaPreviewFrame({
28
34
  <Pressable
29
35
  onPress={onRemove}
30
36
  accessibilityRole="button"
31
- accessibilityLabel="Remover mídia"
32
- style={styles.previewRemove}
37
+ accessibilityLabel={removeLabel}
38
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
39
+ style={({ pressed }) => [styles.previewRemove, pressed && styles.previewRemovePressed]}
33
40
  >
34
41
  <ComposeRemoveGlyph />
35
42
  </Pressable>
@@ -48,10 +55,8 @@ function ComposeRemoveGlyph() {
48
55
 
49
56
  const styles = StyleSheet.create({
50
57
  input: {
58
+ ...m3Type.body,
51
59
  color: m3Colors.onSurface,
52
- fontFamily: fonts.regular,
53
- fontSize: 20,
54
- lineHeight: 28,
55
60
  minHeight: 120,
56
61
  textAlignVertical: 'top',
57
62
  paddingTop: spacing.xs,
@@ -84,6 +89,7 @@ const styles = StyleSheet.create({
84
89
  alignItems: 'center',
85
90
  justifyContent: 'center',
86
91
  },
92
+ previewRemovePressed: { backgroundColor: m3Colors.statePressed },
87
93
  removeGlyph: {
88
94
  width: 14,
89
95
  height: 14,
@@ -2,7 +2,8 @@ import type { ReactNode } from 'react';
2
2
  import { Pressable, StyleSheet } from 'react-native';
3
3
  import { m3Colors } from '../theme/proulr-theme';
4
4
 
5
- const TOOLBAR_ICON_SIZE = 48;
5
+ /** Alvo de toque do toolbar (48pt); dono único — o toolbar só reexporta. */
6
+ export const COMPOSE_TOOLBAR_ICON_SIZE = 48;
6
7
 
7
8
  export function ComposeToolbarIcon({
8
9
  label,
@@ -23,7 +24,7 @@ export function ComposeToolbarIcon({
23
24
  style={({ pressed }) => [
24
25
  styles.icon,
25
26
  active && styles.iconActive,
26
- pressed && { opacity: 0.75 },
27
+ pressed && styles.iconPressed,
27
28
  ]}
28
29
  >
29
30
  {children}
@@ -33,13 +34,16 @@ export function ComposeToolbarIcon({
33
34
 
34
35
  const styles = StyleSheet.create({
35
36
  icon: {
36
- width: TOOLBAR_ICON_SIZE,
37
- height: TOOLBAR_ICON_SIZE,
38
- borderRadius: TOOLBAR_ICON_SIZE / 2,
37
+ width: COMPOSE_TOOLBAR_ICON_SIZE,
38
+ height: COMPOSE_TOOLBAR_ICON_SIZE,
39
+ borderRadius: COMPOSE_TOOLBAR_ICON_SIZE / 2,
39
40
  alignItems: 'center',
40
41
  justifyContent: 'center',
41
42
  },
42
43
  iconActive: {
43
44
  backgroundColor: m3Colors.primaryContainer,
44
45
  },
46
+ iconPressed: {
47
+ opacity: 0.75,
48
+ },
45
49
  });
@@ -2,9 +2,6 @@ import type { ReactNode } from 'react';
2
2
  import { StyleSheet, View } from 'react-native';
3
3
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
4
  import { m3Colors, spacing } from '../theme/proulr-theme';
5
- import { ComposeToolbarIcon } from './compose-toolbar-icon';
6
-
7
- const TOOLBAR_ICON_SIZE = 48;
8
5
 
9
6
  export function ComposeToolbar({
10
7
  children,
@@ -28,8 +25,6 @@ export function ComposeToolbar({
28
25
  );
29
26
  }
30
27
 
31
- export { ComposeToolbarIcon };
32
-
33
28
  const styles = StyleSheet.create({
34
29
  toolbar: {
35
30
  position: 'absolute',
@@ -53,5 +48,3 @@ const styles = StyleSheet.create({
53
48
  bottom: undefined,
54
49
  },
55
50
  });
56
-
57
- export const COMPOSE_TOOLBAR_ICON_SIZE = TOOLBAR_ICON_SIZE;
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { Pressable, StyleSheet, Text } from 'react-native';
3
- import { fonts, m3Colors, spacing } from '../theme/proulr-theme';
3
+ import { m3Colors, m3Type, spacing } from '../theme/proulr-theme';
4
4
  import { AppIcon } from './app-icon';
5
5
 
6
6
  export function ComposeVisibilityRow({
@@ -44,8 +44,7 @@ const styles = StyleSheet.create({
44
44
  borderTopColor: m3Colors.outlineVariant,
45
45
  },
46
46
  label: {
47
+ ...m3Type.label,
47
48
  color: m3Colors.primary,
48
- fontFamily: fonts.medium,
49
- fontSize: 13,
50
49
  },
51
50
  });
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
2
2
  import { useCallback, useState } from 'react';
3
3
  import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
4
4
  import { AppIcon, APP_ICON_SIZE } from './app-icon';
5
- import { fonts, m3Colors, m3Elevation, spacing } from '../theme/proulr-theme';
5
+ import { m3Colors, m3Elevation, m3Type, spacing } from '../theme/proulr-theme';
6
6
 
7
7
  export type FloatingCreateMenuAction = {
8
8
  id: string;
@@ -139,9 +139,8 @@ const styles = StyleSheet.create({
139
139
  paddingVertical: spacing.xs,
140
140
  },
141
141
  label: {
142
+ ...m3Type.titleSmall,
142
143
  color: m3Colors.onSurface,
143
- fontFamily: fonts.semibold,
144
- fontSize: 16,
145
144
  },
146
145
  actionFab: {
147
146
  width: ACTION_FAB_SIZE,
@@ -0,0 +1,26 @@
1
+ import { useCallback, useState } from 'react';
2
+ import type { ViewStyle } from 'react-native';
3
+ import { m3Colors } from '../theme/proulr-theme';
4
+
5
+ /**
6
+ * Anel de foco de teclado (WCAG 2.4.7). `outline*` pinta fora da caixa — não
7
+ * desloca layout — e `borderColor` cobre as variantes sem borda no nativo.
8
+ */
9
+ export const FOCUS_RING_STYLE: ViewStyle = {
10
+ borderColor: m3Colors.focus,
11
+ outlineColor: m3Colors.focus,
12
+ outlineStyle: 'solid',
13
+ outlineWidth: 2,
14
+ outlineOffset: 2,
15
+ };
16
+
17
+ export function useFocusRing(): {
18
+ focused: boolean;
19
+ onFocus: () => void;
20
+ onBlur: () => void;
21
+ } {
22
+ const [focused, setFocused] = useState(false);
23
+ const onFocus = useCallback(() => setFocused(true), []);
24
+ const onBlur = useCallback(() => setFocused(false), []);
25
+ return { focused, onFocus, onBlur };
26
+ }
@@ -51,7 +51,8 @@ export {
51
51
  type ComposePillTextFieldProps,
52
52
  type ComposePillVariant,
53
53
  } from './compose-pill-input';
54
- export { ComposeToolbar, ComposeToolbarIcon, COMPOSE_TOOLBAR_ICON_SIZE } from './compose-toolbar';
54
+ export { ComposeToolbar } from './compose-toolbar';
55
+ export { ComposeToolbarIcon, COMPOSE_TOOLBAR_ICON_SIZE } from './compose-toolbar-icon';
55
56
  export { ComposeReplyAudienceSheet, type ComposeReplyAudienceOption, type ComposeReplyAudienceValue } from './compose-reply-audience-sheet';
56
57
  export { ComposeVisibilityRow } from './compose-visibility-row';
57
58
  export { DateField } from './DateField';
@@ -1,15 +1,20 @@
1
1
  import { Pressable, StyleSheet, Text, View } from 'react-native';
2
2
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
3
- import { fonts, m3Colors, m3Type, spacing } from '../theme/proulr-theme';
3
+ import { fonts, m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
4
4
  import { AppIcon } from './app-icon';
5
5
  import { StorySettingsGlyph } from './story-glyphs';
6
6
 
7
7
  export function StoryComposeHeader({
8
8
  title,
9
+ closeLabel,
10
+ settingsLabel,
9
11
  onClose,
10
12
  onSettings,
11
13
  }: {
12
14
  title: string;
15
+ /** Rótulos accessíveis vêm do chamador (copy via Rosetta) — o renderer não traduz. */
16
+ closeLabel: string;
17
+ settingsLabel?: string;
13
18
  onClose: () => void;
14
19
  onSettings?: () => void;
15
20
  }) {
@@ -19,10 +24,9 @@ export function StoryComposeHeader({
19
24
  <View style={[styles.header, { paddingTop: insets.top + spacing.xs }]}>
20
25
  <Pressable
21
26
  onPress={onClose}
22
- hitSlop={10}
23
27
  accessibilityRole="button"
24
- accessibilityLabel="Fechar"
25
- style={styles.sideBtn}
28
+ accessibilityLabel={closeLabel}
29
+ style={({ pressed }) => [styles.sideBtn, pressed && styles.sideBtnPressed]}
26
30
  >
27
31
  <AppIcon name="close" color={m3Colors.onSurface} size={18} />
28
32
  </Pressable>
@@ -34,10 +38,9 @@ export function StoryComposeHeader({
34
38
  {onSettings ? (
35
39
  <Pressable
36
40
  onPress={onSettings}
37
- hitSlop={10}
38
41
  accessibilityRole="button"
39
- accessibilityLabel="Configurações do story"
40
- style={styles.sideBtn}
42
+ accessibilityLabel={settingsLabel}
43
+ style={({ pressed }) => [styles.sideBtn, pressed && styles.sideBtnPressed]}
41
44
  >
42
45
  <StorySettingsGlyph />
43
46
  </Pressable>
@@ -57,11 +60,13 @@ const styles = StyleSheet.create({
57
60
  backgroundColor: m3Colors.surface,
58
61
  },
59
62
  sideBtn: {
60
- width: 40,
61
- height: 40,
63
+ width: 44,
64
+ height: 44,
62
65
  alignItems: 'center',
63
66
  justifyContent: 'center',
67
+ borderRadius: m3Shape.cornerFull,
64
68
  },
69
+ sideBtnPressed: { backgroundColor: m3Colors.statePressed },
65
70
  title: {
66
71
  flex: 1,
67
72
  color: m3Colors.onSurface,
@@ -74,9 +74,9 @@ const styles = StyleSheet.create({
74
74
  backgroundColor: m3Colors.secondary,
75
75
  },
76
76
  badgeText: {
77
+ ...m3Type.labelSmall,
77
78
  color: m3Colors.onSecondary,
78
79
  fontFamily: fonts.bold,
79
- fontSize: 10,
80
80
  },
81
81
  iconWrap: {
82
82
  marginBottom: spacing.xs,