@proulr/sdui-components 0.1.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 (57) hide show
  1. package/README.md +29 -0
  2. package/package.json +60 -0
  3. package/src/bdc/compile-bdc-to-wire.ts +123 -0
  4. package/src/bdc/components/discover.tsx +55 -0
  5. package/src/bdc/components/forms.tsx +20 -0
  6. package/src/bdc/components/native-blocks.tsx +35 -0
  7. package/src/bdc/components/social.tsx +27 -0
  8. package/src/bdc/components/ui.tsx +114 -0
  9. package/src/bdc/index.ts +41 -0
  10. package/src/bdc/jsx-runtime.ts +49 -0
  11. package/src/catalog/block-types.ts +18 -0
  12. package/src/catalog/style-tokens.ts +48 -0
  13. package/src/global.d.ts +1 -0
  14. package/src/renderer/SduiScreenRenderer.tsx +28 -0
  15. package/src/renderer/ads/native-ad-catalog.ts +23 -0
  16. package/src/renderer/blocks/SduiDiscoverBlocks.tsx +470 -0
  17. package/src/renderer/blocks/SduiLayoutBlocks.tsx +177 -0
  18. package/src/renderer/blocks/SduiNativeAdBlock.tsx +201 -0
  19. package/src/renderer/blocks/SduiNativeAdBlock.web.tsx +6 -0
  20. package/src/renderer/blocks/SduiNativeDomainBlocks.tsx +141 -0
  21. package/src/renderer/blocks/SduiPrimitiveBlocks.tsx +158 -0
  22. package/src/renderer/blocks/SduiSocialBlocks.tsx +64 -0
  23. package/src/renderer/index.ts +45 -0
  24. package/src/renderer/lib/brazilian-date-input.ts +22 -0
  25. package/src/renderer/lib/discover-profile-formatters.ts +75 -0
  26. package/src/renderer/lib/resolve-data-ref.ts +21 -0
  27. package/src/renderer/sdui-chrome-scroll-context.tsx +40 -0
  28. package/src/renderer/sdui-component-registry.tsx +129 -0
  29. package/src/renderer/sdui-hybrid-renderer-context.tsx +91 -0
  30. package/src/renderer/sdui-layout-debug.tsx +107 -0
  31. package/src/renderer/theme/proulr-theme.ts +164 -0
  32. package/src/renderer/theme/resolve-style-props.ts +85 -0
  33. package/src/renderer/ui/AppButton.tsx +71 -0
  34. package/src/renderer/ui/AppLoadingSpinner.tsx +38 -0
  35. package/src/renderer/ui/AuthStepShell.tsx +128 -0
  36. package/src/renderer/ui/Card.tsx +13 -0
  37. package/src/renderer/ui/DateField.tsx +36 -0
  38. package/src/renderer/ui/InfoBanner.tsx +51 -0
  39. package/src/renderer/ui/TextField.tsx +81 -0
  40. package/src/renderer/ui/compose-audience-pill.tsx +47 -0
  41. package/src/renderer/ui/compose-glyphs.tsx +400 -0
  42. package/src/renderer/ui/compose-media-carousel.tsx +116 -0
  43. package/src/renderer/ui/compose-media-shortcut.tsx +80 -0
  44. package/src/renderer/ui/compose-recent-media-grid.tsx +268 -0
  45. package/src/renderer/ui/compose-reply-audience-sheet.tsx +155 -0
  46. package/src/renderer/ui/compose-screen-header.tsx +115 -0
  47. package/src/renderer/ui/compose-text-area.tsx +94 -0
  48. package/src/renderer/ui/compose-toolbar-icon.tsx +45 -0
  49. package/src/renderer/ui/compose-toolbar.tsx +57 -0
  50. package/src/renderer/ui/compose-visibility-row.tsx +51 -0
  51. package/src/renderer/ui/floating-create-menu.tsx +165 -0
  52. package/src/renderer/ui/index.ts +44 -0
  53. package/src/renderer/ui/story-compose-header.tsx +72 -0
  54. package/src/renderer/ui/story-create-shortcut-row.tsx +89 -0
  55. package/src/renderer/ui/story-editor-chrome.tsx +231 -0
  56. package/src/renderer/ui/story-glyphs.tsx +144 -0
  57. package/src/renderer/ui/story-surface-picker.tsx +124 -0
@@ -0,0 +1,268 @@
1
+ import { useMemo } from 'react';
2
+ import {
3
+ Image,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ useWindowDimensions,
8
+ View,
9
+ } from 'react-native';
10
+ import { fonts, m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
11
+ import { AppLoadingSpinner } from './AppLoadingSpinner';
12
+ import { ComposeCameraGlyph } from './compose-glyphs';
13
+ import { StoryTextGlyph } from './story-glyphs';
14
+
15
+ export type ComposeRecentMediaItem = {
16
+ id: string;
17
+ uri: string;
18
+ mediaType: 'photo' | 'video';
19
+ duration?: number;
20
+ };
21
+
22
+ const GRID_COLUMNS = 3;
23
+ const GRID_GAP = 2;
24
+ const GRID_PANEL_MIN_HEIGHT = 220;
25
+ const GRID_MAX_CELLS = 30;
26
+
27
+ type GridCell =
28
+ | { id: string; kind: 'camera' }
29
+ | { id: string; kind: 'text' }
30
+ | (ComposeRecentMediaItem & { kind: 'asset' });
31
+
32
+ function chunkGridRows(cells: GridCell[], columns: number): GridCell[][] {
33
+ const rows: GridCell[][] = [];
34
+ for (let index = 0; index < cells.length; index += columns) {
35
+ rows.push(cells.slice(index, index + columns));
36
+ }
37
+ return rows;
38
+ }
39
+
40
+ export function ComposeRecentMediaGrid({
41
+ items,
42
+ loading,
43
+ selectedId,
44
+ selectedIds,
45
+ emptyHint,
46
+ onSelectCamera,
47
+ onSelectText,
48
+ onSelectItem,
49
+ onSelectFromLibrary,
50
+ }: {
51
+ items: ComposeRecentMediaItem[];
52
+ loading?: boolean;
53
+ selectedId?: string | null;
54
+ selectedIds?: string[];
55
+ emptyHint?: string;
56
+ onSelectCamera: () => void;
57
+ onSelectText?: () => void;
58
+ onSelectItem: (item: ComposeRecentMediaItem) => void;
59
+ onSelectFromLibrary: () => void;
60
+ }) {
61
+ const { width } = useWindowDimensions();
62
+ const cellSize = useMemo(() => {
63
+ const horizontalPad = spacing.md * 2;
64
+ return Math.floor((width - horizontalPad - GRID_GAP * (GRID_COLUMNS - 1)) / GRID_COLUMNS);
65
+ }, [width]);
66
+
67
+ const gridRows = useMemo(() => {
68
+ const reservedSlots = 1 + (onSelectText ? 1 : 0);
69
+ const cells: GridCell[] = [
70
+ { id: '__camera__', kind: 'camera' },
71
+ ...(onSelectText ? [{ id: '__text__', kind: 'text' as const }] : []),
72
+ ...items.slice(0, GRID_MAX_CELLS - reservedSlots).map((item) => ({ ...item, kind: 'asset' as const })),
73
+ ];
74
+ return chunkGridRows(cells, GRID_COLUMNS);
75
+ }, [items, onSelectText]);
76
+
77
+ return (
78
+ <View style={styles.panel}>
79
+ <View style={styles.header}>
80
+ <Text style={styles.albumLabel}>Recentes</Text>
81
+
82
+ <Pressable
83
+ onPress={onSelectFromLibrary}
84
+ accessibilityRole="button"
85
+ accessibilityLabel="Selecionar da galeria"
86
+ style={({ pressed }) => [styles.selectBtn, pressed && { opacity: 0.85 }]}
87
+ >
88
+ <Text style={styles.selectLabel}>Selecionar</Text>
89
+ </Pressable>
90
+ </View>
91
+
92
+ {loading ? (
93
+ <View style={[styles.loadingWrap, { minHeight: GRID_PANEL_MIN_HEIGHT }]}>
94
+ <AppLoadingSpinner />
95
+ </View>
96
+ ) : (
97
+ <View style={styles.grid}>
98
+ {items.length === 0 && emptyHint ? (
99
+ <Text style={styles.emptyHint}>{emptyHint}</Text>
100
+ ) : null}
101
+ {gridRows.map((row, rowIndex) => (
102
+ <View key={`row-${rowIndex}`} style={styles.gridRow}>
103
+ {row.map((item) => {
104
+ if (item.kind === 'camera') {
105
+ return (
106
+ <Pressable
107
+ key={item.id}
108
+ onPress={onSelectCamera}
109
+ accessibilityRole="button"
110
+ accessibilityLabel="Abrir câmera"
111
+ style={({ pressed }) => [
112
+ styles.cell,
113
+ { width: cellSize, height: cellSize },
114
+ pressed && { opacity: 0.85 },
115
+ ]}
116
+ >
117
+ <ComposeCameraGlyph color={m3Colors.onSurface} />
118
+ </Pressable>
119
+ );
120
+ }
121
+
122
+ if (item.kind === 'text') {
123
+ return (
124
+ <Pressable
125
+ key={item.id}
126
+ onPress={onSelectText}
127
+ accessibilityRole="button"
128
+ accessibilityLabel="Criar story de texto"
129
+ style={({ pressed }) => [
130
+ styles.cell,
131
+ { width: cellSize, height: cellSize },
132
+ pressed && { opacity: 0.85 },
133
+ ]}
134
+ >
135
+ <StoryTextGlyph color={m3Colors.onSurface} size={32} />
136
+ <Text style={styles.textCellLabel}>Texto</Text>
137
+ </Pressable>
138
+ );
139
+ }
140
+
141
+ const selected =
142
+ selectedIds?.includes(item.id) || selectedId === item.id;
143
+ return (
144
+ <Pressable
145
+ key={item.id}
146
+ onPress={() => onSelectItem(item)}
147
+ accessibilityRole="button"
148
+ accessibilityLabel={item.mediaType === 'video' ? 'Selecionar vídeo' : 'Selecionar foto'}
149
+ style={({ pressed }) => [
150
+ styles.cell,
151
+ { width: cellSize, height: cellSize },
152
+ selected && styles.cellSelected,
153
+ pressed && { opacity: 0.9 },
154
+ ]}
155
+ >
156
+ <Image source={{ uri: item.uri }} style={styles.thumb} resizeMode="cover" />
157
+ {item.mediaType === 'video' ? (
158
+ <View style={styles.videoBadge}>
159
+ <Text style={styles.videoBadgeText}>
160
+ {formatVideoDuration(item.duration)}
161
+ </Text>
162
+ </View>
163
+ ) : null}
164
+ </Pressable>
165
+ );
166
+ })}
167
+ </View>
168
+ ))}
169
+ </View>
170
+ )}
171
+ </View>
172
+ );
173
+ }
174
+
175
+ function formatVideoDuration(duration?: number): string {
176
+ if (!duration || duration <= 0) return 'Vídeo';
177
+ const total = Math.round(duration);
178
+ const minutes = Math.floor(total / 60);
179
+ const seconds = total % 60;
180
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`;
181
+ }
182
+
183
+ const styles = StyleSheet.create({
184
+ panel: {
185
+ marginTop: spacing.md,
186
+ borderTopWidth: StyleSheet.hairlineWidth,
187
+ borderTopColor: m3Colors.outlineVariant,
188
+ backgroundColor: m3Colors.surfaceContainer,
189
+ paddingTop: spacing.sm,
190
+ marginHorizontal: -spacing.md,
191
+ paddingHorizontal: spacing.md,
192
+ paddingBottom: spacing.sm,
193
+ },
194
+ header: {
195
+ flexDirection: 'row',
196
+ alignItems: 'center',
197
+ justifyContent: 'space-between',
198
+ marginBottom: spacing.sm,
199
+ },
200
+ albumLabel: {
201
+ color: m3Colors.onSurface,
202
+ ...m3Type.label,
203
+ },
204
+ selectBtn: {
205
+ paddingHorizontal: spacing.md,
206
+ paddingVertical: spacing.xs,
207
+ borderRadius: m3Shape.cornerFull,
208
+ backgroundColor: m3Colors.surfaceContainerHigh,
209
+ borderWidth: 1,
210
+ borderColor: m3Colors.outlineVariant,
211
+ },
212
+ selectLabel: {
213
+ color: m3Colors.onSurface,
214
+ fontFamily: fonts.medium,
215
+ fontSize: 13,
216
+ },
217
+ loadingWrap: {
218
+ alignItems: 'center',
219
+ justifyContent: 'center',
220
+ },
221
+ emptyHint: {
222
+ color: m3Colors.onSurfaceVariant,
223
+ ...m3Type.bodySmall,
224
+ marginBottom: spacing.sm,
225
+ },
226
+ grid: {
227
+ gap: GRID_GAP,
228
+ },
229
+ gridRow: {
230
+ flexDirection: 'row',
231
+ gap: GRID_GAP,
232
+ },
233
+ cell: {
234
+ borderRadius: m3Shape.cornerSmall,
235
+ backgroundColor: m3Colors.surfaceContainerHigh,
236
+ alignItems: 'center',
237
+ justifyContent: 'center',
238
+ overflow: 'hidden',
239
+ },
240
+ cellSelected: {
241
+ borderWidth: 2,
242
+ borderColor: m3Colors.primary,
243
+ },
244
+ thumb: {
245
+ width: '100%',
246
+ height: '100%',
247
+ },
248
+ videoBadge: {
249
+ position: 'absolute',
250
+ left: spacing.xs,
251
+ bottom: spacing.xs,
252
+ paddingHorizontal: spacing.xs,
253
+ paddingVertical: 2,
254
+ borderRadius: m3Shape.cornerExtraSmall,
255
+ backgroundColor: m3Colors.scrim,
256
+ },
257
+ videoBadgeText: {
258
+ color: m3Colors.onPrimary,
259
+ fontFamily: fonts.medium,
260
+ fontSize: 10,
261
+ },
262
+ textCellLabel: {
263
+ marginTop: spacing.xs,
264
+ color: m3Colors.onSurfaceVariant,
265
+ fontFamily: fonts.medium,
266
+ fontSize: 11,
267
+ },
268
+ });
@@ -0,0 +1,155 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
3
+ import { fonts, m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
4
+
5
+ export type ComposeReplyAudienceValue = 'everyone' | 'followers' | 'mentioned';
6
+
7
+ export type ComposeReplyAudienceOption = {
8
+ value: ComposeReplyAudienceValue;
9
+ label: string;
10
+ hint: string;
11
+ icon: ReactNode;
12
+ };
13
+
14
+ export function ComposeReplyAudienceSheet({
15
+ visible,
16
+ value,
17
+ options,
18
+ onClose,
19
+ onSelect,
20
+ }: {
21
+ visible: boolean;
22
+ value: ComposeReplyAudienceValue;
23
+ options: ComposeReplyAudienceOption[];
24
+ onClose: () => void;
25
+ onSelect: (value: ComposeReplyAudienceValue) => void;
26
+ }) {
27
+ return (
28
+ <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
29
+ <View style={styles.backdrop}>
30
+ <Pressable style={styles.backdropPress} onPress={onClose} accessibilityLabel="Fechar" />
31
+ <View style={styles.sheet}>
32
+ <View style={styles.handle} />
33
+ <Text style={styles.title}>Quem pode responder?</Text>
34
+ <Text style={styles.subtitle}>
35
+ Escolha quem pode responder a este post. Perfis mencionados sempre poderão responder.
36
+ </Text>
37
+
38
+ {options.map((option) => {
39
+ const selected = option.value === value;
40
+ return (
41
+ <Pressable
42
+ key={option.value}
43
+ onPress={() => {
44
+ onSelect(option.value);
45
+ onClose();
46
+ }}
47
+ accessibilityRole="button"
48
+ accessibilityState={{ selected }}
49
+ style={({ pressed }) => [styles.option, pressed && { opacity: 0.86 }]}
50
+ >
51
+ <View style={styles.optionIconWrap}>{option.icon}</View>
52
+ <View style={styles.optionBody}>
53
+ <Text style={styles.optionLabel}>{option.label}</Text>
54
+ <Text style={styles.optionHint}>{option.hint}</Text>
55
+ </View>
56
+ {selected ? (
57
+ <View style={styles.checkBadge}>
58
+ <Text style={styles.checkMark}>{'\u2713'}</Text>
59
+ </View>
60
+ ) : (
61
+ <View style={styles.checkSpacer} />
62
+ )}
63
+ </Pressable>
64
+ );
65
+ })}
66
+ </View>
67
+ </View>
68
+ </Modal>
69
+ );
70
+ }
71
+
72
+ const styles = StyleSheet.create({
73
+ backdrop: {
74
+ flex: 1,
75
+ justifyContent: 'flex-end',
76
+ },
77
+ backdropPress: {
78
+ ...StyleSheet.absoluteFill,
79
+ backgroundColor: m3Colors.scrim,
80
+ },
81
+ sheet: {
82
+ backgroundColor: m3Colors.surface,
83
+ borderTopLeftRadius: m3Shape.cornerExtraLarge,
84
+ borderTopRightRadius: m3Shape.cornerExtraLarge,
85
+ paddingHorizontal: spacing.lg,
86
+ paddingBottom: spacing.xl,
87
+ borderTopWidth: StyleSheet.hairlineWidth,
88
+ borderTopColor: m3Colors.outlineVariant,
89
+ },
90
+ handle: {
91
+ alignSelf: 'center',
92
+ width: 40,
93
+ height: 4,
94
+ borderRadius: 2,
95
+ backgroundColor: m3Colors.outline,
96
+ marginTop: spacing.sm,
97
+ marginBottom: spacing.md,
98
+ },
99
+ title: {
100
+ color: m3Colors.onSurface,
101
+ ...m3Type.titleSmall,
102
+ fontFamily: fonts.bold,
103
+ marginBottom: spacing.xs,
104
+ },
105
+ subtitle: {
106
+ color: m3Colors.onSurfaceVariant,
107
+ ...m3Type.bodySmall,
108
+ marginBottom: spacing.md,
109
+ lineHeight: 18,
110
+ },
111
+ option: {
112
+ flexDirection: 'row',
113
+ alignItems: 'center',
114
+ paddingVertical: spacing.md,
115
+ gap: spacing.md,
116
+ },
117
+ optionIconWrap: {
118
+ width: 44,
119
+ height: 44,
120
+ borderRadius: 22,
121
+ backgroundColor: m3Colors.primary,
122
+ alignItems: 'center',
123
+ justifyContent: 'center',
124
+ },
125
+ optionBody: {
126
+ flex: 1,
127
+ minWidth: 0,
128
+ },
129
+ optionLabel: {
130
+ color: m3Colors.onSurface,
131
+ fontFamily: fonts.semibold,
132
+ fontSize: 16,
133
+ },
134
+ optionHint: {
135
+ color: m3Colors.onSurfaceVariant,
136
+ ...m3Type.bodySmall,
137
+ marginTop: 2,
138
+ },
139
+ checkBadge: {
140
+ width: 24,
141
+ height: 24,
142
+ borderRadius: 12,
143
+ backgroundColor: m3Colors.primary,
144
+ alignItems: 'center',
145
+ justifyContent: 'center',
146
+ },
147
+ checkMark: {
148
+ color: m3Colors.onPrimary,
149
+ fontSize: 14,
150
+ fontFamily: fonts.bold,
151
+ },
152
+ checkSpacer: {
153
+ width: 24,
154
+ },
155
+ });
@@ -0,0 +1,115 @@
1
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
2
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
3
+ import { fonts, m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
4
+ import { AppLoadingSpinner } from './AppLoadingSpinner';
5
+ import { ComposeCloseGlyph } from './compose-glyphs';
6
+
7
+ export function ComposeScreenHeader({
8
+ title,
9
+ actionLabel = 'Publicar',
10
+ canAction,
11
+ loading,
12
+ onClose,
13
+ onAction,
14
+ }: {
15
+ title?: string;
16
+ actionLabel?: string;
17
+ canAction: boolean;
18
+ loading?: boolean;
19
+ onClose: () => void;
20
+ onAction: () => void;
21
+ }) {
22
+ const insets = useSafeAreaInsets();
23
+
24
+ return (
25
+ <View style={[styles.header, { paddingTop: insets.top + spacing.xs }]}>
26
+ <Pressable
27
+ onPress={onClose}
28
+ hitSlop={10}
29
+ accessibilityRole="button"
30
+ accessibilityLabel="Fechar"
31
+ style={styles.closeBtn}
32
+ >
33
+ <ComposeCloseGlyph />
34
+ </Pressable>
35
+
36
+ {title ? (
37
+ <Text style={styles.title} numberOfLines={1}>
38
+ {title}
39
+ </Text>
40
+ ) : (
41
+ <View style={styles.titleSpacer} />
42
+ )}
43
+
44
+ <Pressable
45
+ onPress={onAction}
46
+ disabled={!canAction || loading}
47
+ accessibilityRole="button"
48
+ accessibilityLabel={actionLabel}
49
+ style={({ pressed }) => [
50
+ styles.publishPill,
51
+ canAction && styles.publishPillActive,
52
+ (!canAction || loading) && styles.publishPillDisabled,
53
+ pressed && canAction && { opacity: 0.88 },
54
+ ]}
55
+ >
56
+ {loading ? (
57
+ <AppLoadingSpinner color={m3Colors.onPrimary} />
58
+ ) : (
59
+ <Text style={[styles.publishLabel, canAction && styles.publishLabelActive]}>{actionLabel}</Text>
60
+ )}
61
+ </Pressable>
62
+ </View>
63
+ );
64
+ }
65
+
66
+ const styles = StyleSheet.create({
67
+ header: {
68
+ flexDirection: 'row',
69
+ alignItems: 'center',
70
+ paddingHorizontal: spacing.md,
71
+ paddingBottom: spacing.sm,
72
+ gap: spacing.sm,
73
+ borderBottomWidth: StyleSheet.hairlineWidth,
74
+ borderBottomColor: m3Colors.outlineVariant,
75
+ backgroundColor: m3Colors.surface,
76
+ },
77
+ closeBtn: {
78
+ width: 40,
79
+ height: 40,
80
+ alignItems: 'center',
81
+ justifyContent: 'center',
82
+ },
83
+ title: {
84
+ flex: 1,
85
+ color: m3Colors.onSurface,
86
+ ...m3Type.titleSmall,
87
+ textAlign: 'center',
88
+ },
89
+ titleSpacer: {
90
+ flex: 1,
91
+ },
92
+ publishPill: {
93
+ minWidth: 88,
94
+ height: 36,
95
+ borderRadius: m3Shape.cornerFull,
96
+ alignItems: 'center',
97
+ justifyContent: 'center',
98
+ paddingHorizontal: spacing.md,
99
+ backgroundColor: m3Colors.surfaceContainerHigh,
100
+ },
101
+ publishPillActive: {
102
+ backgroundColor: m3Colors.primary,
103
+ },
104
+ publishPillDisabled: {
105
+ opacity: 0.45,
106
+ },
107
+ publishLabel: {
108
+ color: m3Colors.onSurfaceVariant,
109
+ fontFamily: fonts.semibold,
110
+ fontSize: 14,
111
+ },
112
+ publishLabelActive: {
113
+ color: m3Colors.onPrimary,
114
+ },
115
+ });
@@ -0,0 +1,94 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Platform, Pressable, StyleSheet, TextInput, TextInputProps, View } from 'react-native';
3
+ import { fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
4
+
5
+ export function ComposeTextArea(props: TextInputProps) {
6
+ return (
7
+ <TextInput
8
+ placeholder="O que está acontecendo?"
9
+ placeholderTextColor={m3Colors.onSurfaceVariant}
10
+ multiline
11
+ style={styles.input}
12
+ {...(Platform.OS === 'android' ? { includeFontPadding: false } : {})}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ export function ComposeMediaPreviewFrame({
19
+ children,
20
+ onRemove,
21
+ }: {
22
+ children: ReactNode;
23
+ onRemove: () => void;
24
+ }) {
25
+ return (
26
+ <View style={styles.previewWrap}>
27
+ <View style={styles.preview}>{children}</View>
28
+ <Pressable
29
+ onPress={onRemove}
30
+ accessibilityRole="button"
31
+ accessibilityLabel="Remover mídia"
32
+ style={styles.previewRemove}
33
+ >
34
+ <ComposeRemoveGlyph />
35
+ </Pressable>
36
+ </View>
37
+ );
38
+ }
39
+
40
+ function ComposeRemoveGlyph() {
41
+ return (
42
+ <View style={styles.removeGlyph}>
43
+ <View style={[styles.removeLine, { transform: [{ rotate: '45deg' }] }]} />
44
+ <View style={[styles.removeLine, { transform: [{ rotate: '-45deg' }] }]} />
45
+ </View>
46
+ );
47
+ }
48
+
49
+ const styles = StyleSheet.create({
50
+ input: {
51
+ color: m3Colors.onSurface,
52
+ fontFamily: fonts.regular,
53
+ fontSize: 20,
54
+ lineHeight: 28,
55
+ minHeight: 120,
56
+ textAlignVertical: 'top',
57
+ paddingTop: spacing.xs,
58
+ },
59
+ previewWrap: {
60
+ position: 'relative',
61
+ marginTop: spacing.xs,
62
+ },
63
+ preview: {
64
+ width: '100%',
65
+ aspectRatio: 1,
66
+ borderRadius: m3Shape.cornerLarge,
67
+ backgroundColor: m3Colors.surfaceContainerHigh,
68
+ overflow: 'hidden',
69
+ },
70
+ previewRemove: {
71
+ position: 'absolute',
72
+ top: spacing.sm,
73
+ right: spacing.sm,
74
+ width: 36,
75
+ height: 36,
76
+ borderRadius: 18,
77
+ backgroundColor: m3Colors.scrim,
78
+ alignItems: 'center',
79
+ justifyContent: 'center',
80
+ },
81
+ removeGlyph: {
82
+ width: 14,
83
+ height: 14,
84
+ alignItems: 'center',
85
+ justifyContent: 'center',
86
+ },
87
+ removeLine: {
88
+ position: 'absolute',
89
+ width: 14,
90
+ height: 2,
91
+ borderRadius: 1,
92
+ backgroundColor: m3Colors.onPrimary,
93
+ },
94
+ });
@@ -0,0 +1,45 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Pressable, StyleSheet } from 'react-native';
3
+ import { m3Colors } from '../theme/proulr-theme';
4
+
5
+ const TOOLBAR_ICON_SIZE = 48;
6
+
7
+ export function ComposeToolbarIcon({
8
+ label,
9
+ active,
10
+ children,
11
+ onPress,
12
+ }: {
13
+ label: string;
14
+ active?: boolean;
15
+ children: ReactNode;
16
+ onPress: () => void;
17
+ }) {
18
+ return (
19
+ <Pressable
20
+ onPress={onPress}
21
+ accessibilityRole="button"
22
+ accessibilityLabel={label}
23
+ style={({ pressed }) => [
24
+ styles.icon,
25
+ active && styles.iconActive,
26
+ pressed && { opacity: 0.75 },
27
+ ]}
28
+ >
29
+ {children}
30
+ </Pressable>
31
+ );
32
+ }
33
+
34
+ const styles = StyleSheet.create({
35
+ icon: {
36
+ width: TOOLBAR_ICON_SIZE,
37
+ height: TOOLBAR_ICON_SIZE,
38
+ borderRadius: TOOLBAR_ICON_SIZE / 2,
39
+ alignItems: 'center',
40
+ justifyContent: 'center',
41
+ },
42
+ iconActive: {
43
+ backgroundColor: m3Colors.primaryContainer,
44
+ },
45
+ });