@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,57 @@
1
+ import type { ReactNode } from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
+ import { m3Colors, spacing } from '../theme/proulr-theme';
5
+ import { ComposeToolbarIcon } from './compose-toolbar-icon';
6
+
7
+ const TOOLBAR_ICON_SIZE = 48;
8
+
9
+ export function ComposeToolbar({
10
+ children,
11
+ bottomInset = 0,
12
+ inline = false,
13
+ }: {
14
+ children: ReactNode;
15
+ bottomInset?: number;
16
+ inline?: boolean;
17
+ }) {
18
+ const insets = useSafeAreaInsets();
19
+ const padBottom = inline ? Math.max(insets.bottom, bottomInset) + spacing.xs : bottomInset;
20
+
21
+ return (
22
+ <View
23
+ style={[styles.toolbar, inline && styles.toolbarInline, { paddingBottom: padBottom }]}
24
+ pointerEvents="box-none"
25
+ >
26
+ {children}
27
+ </View>
28
+ );
29
+ }
30
+
31
+ export { ComposeToolbarIcon };
32
+
33
+ const styles = StyleSheet.create({
34
+ toolbar: {
35
+ position: 'absolute',
36
+ left: 0,
37
+ right: 0,
38
+ bottom: 0,
39
+ flexDirection: 'row',
40
+ alignItems: 'center',
41
+ gap: spacing.xs,
42
+ paddingHorizontal: spacing.md,
43
+ paddingTop: spacing.sm,
44
+ borderTopWidth: StyleSheet.hairlineWidth,
45
+ borderTopColor: m3Colors.outlineVariant,
46
+ backgroundColor: m3Colors.surface,
47
+ zIndex: 2,
48
+ },
49
+ toolbarInline: {
50
+ position: 'relative',
51
+ left: undefined,
52
+ right: undefined,
53
+ bottom: undefined,
54
+ },
55
+ });
56
+
57
+ export const COMPOSE_TOOLBAR_ICON_SIZE = TOOLBAR_ICON_SIZE;
@@ -0,0 +1,51 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Pressable, StyleSheet, Text } from 'react-native';
3
+ import { fonts, m3Colors, spacing } from '../theme/proulr-theme';
4
+ import { ComposeGlobeGlyph } from './compose-glyphs';
5
+
6
+ export function ComposeVisibilityRow({
7
+ label,
8
+ icon,
9
+ onPress,
10
+ variant = 'footer',
11
+ }: {
12
+ label: string;
13
+ icon?: ReactNode;
14
+ onPress?: () => void;
15
+ variant?: 'inline' | 'footer';
16
+ }) {
17
+ return (
18
+ <Pressable
19
+ onPress={onPress}
20
+ disabled={!onPress}
21
+ accessibilityRole="button"
22
+ accessibilityLabel={label}
23
+ style={({ pressed }) => [
24
+ styles.row,
25
+ variant === 'footer' && styles.rowFooter,
26
+ pressed && onPress && { opacity: 0.82 },
27
+ ]}
28
+ >
29
+ {icon ?? <ComposeGlobeGlyph />}
30
+ <Text style={styles.label}>{label}</Text>
31
+ </Pressable>
32
+ );
33
+ }
34
+
35
+ const styles = StyleSheet.create({
36
+ row: {
37
+ flexDirection: 'row',
38
+ alignItems: 'center',
39
+ gap: spacing.xs,
40
+ },
41
+ rowFooter: {
42
+ paddingTop: spacing.sm,
43
+ borderTopWidth: StyleSheet.hairlineWidth,
44
+ borderTopColor: m3Colors.outlineVariant,
45
+ },
46
+ label: {
47
+ color: m3Colors.primary,
48
+ fontFamily: fonts.medium,
49
+ fontSize: 13,
50
+ },
51
+ });
@@ -0,0 +1,165 @@
1
+ import type { ReactNode } from 'react';
2
+ import { useCallback, useState } from 'react';
3
+ import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
4
+ import { fonts, m3Colors, m3Elevation, m3Shape, spacing } from '../theme/proulr-theme';
5
+
6
+ export type FloatingCreateMenuAction = {
7
+ id: string;
8
+ label: string;
9
+ tone?: 'primary' | 'surface';
10
+ icon: ReactNode;
11
+ onPress: () => void;
12
+ };
13
+
14
+ const MAIN_FAB_SIZE = 56;
15
+ const ACTION_FAB_SIZE = 48;
16
+
17
+ export function FloatingCreateMenu({
18
+ bottomInset,
19
+ actions,
20
+ accessibilityLabel = 'Criar conteúdo',
21
+ }: {
22
+ bottomInset: number;
23
+ actions: FloatingCreateMenuAction[];
24
+ accessibilityLabel?: string;
25
+ }) {
26
+ const [open, setOpen] = useState(false);
27
+
28
+ const close = useCallback(() => setOpen(false), []);
29
+
30
+ const onActionPress = useCallback(
31
+ (action: FloatingCreateMenuAction) => {
32
+ close();
33
+ action.onPress();
34
+ },
35
+ [close],
36
+ );
37
+
38
+ const orderedActions = actions;
39
+
40
+ return (
41
+ <>
42
+ <Pressable
43
+ onPress={() => setOpen((value) => !value)}
44
+ accessibilityRole="button"
45
+ accessibilityLabel={open ? 'Fechar menu de criação' : accessibilityLabel}
46
+ style={({ pressed }) => [
47
+ styles.mainFab,
48
+ { bottom: bottomInset + spacing.sm },
49
+ open && styles.mainFabOpen,
50
+ pressed && { opacity: 0.9 },
51
+ ]}
52
+ >
53
+ <Text style={[styles.mainFabGlyph, open && styles.mainFabGlyphOpen]}>{open ? '\u2715' : '+'}</Text>
54
+ </Pressable>
55
+
56
+ <Modal visible={open} transparent animationType="fade" onRequestClose={close}>
57
+ <View style={styles.overlay}>
58
+ <Pressable style={styles.backdrop} onPress={close} accessibilityLabel="Fechar menu" />
59
+
60
+ <View style={[styles.stack, { bottom: bottomInset + spacing.sm + MAIN_FAB_SIZE + spacing.md }]}>
61
+ {orderedActions.map((action) => {
62
+ const isPrimary = action.tone === 'primary';
63
+ return (
64
+ <View key={action.id} style={styles.row}>
65
+ <Pressable
66
+ onPress={() => onActionPress(action)}
67
+ accessibilityRole="button"
68
+ accessibilityLabel={action.label}
69
+ style={({ pressed }) => [styles.labelBtn, pressed && { opacity: 0.85 }]}
70
+ >
71
+ <Text style={styles.label}>{action.label}</Text>
72
+ </Pressable>
73
+
74
+ <Pressable
75
+ onPress={() => onActionPress(action)}
76
+ accessibilityRole="button"
77
+ accessibilityLabel={action.label}
78
+ style={({ pressed }) => [
79
+ styles.actionFab,
80
+ isPrimary ? styles.actionFabPrimary : styles.actionFabSurface,
81
+ pressed && { opacity: 0.9 },
82
+ ]}
83
+ >
84
+ {action.icon}
85
+ </Pressable>
86
+ </View>
87
+ );
88
+ })}
89
+ </View>
90
+ </View>
91
+ </Modal>
92
+ </>
93
+ );
94
+ }
95
+
96
+ const styles = StyleSheet.create({
97
+ mainFab: {
98
+ position: 'absolute',
99
+ right: spacing.sm,
100
+ width: MAIN_FAB_SIZE,
101
+ height: MAIN_FAB_SIZE,
102
+ borderRadius: MAIN_FAB_SIZE / 2,
103
+ backgroundColor: m3Colors.secondary,
104
+ alignItems: 'center',
105
+ justifyContent: 'center',
106
+ zIndex: 20,
107
+ ...m3Elevation.level3,
108
+ },
109
+ mainFabOpen: {
110
+ backgroundColor: m3Colors.surfaceContainerHighest,
111
+ borderWidth: 1,
112
+ borderColor: m3Colors.outlineVariant,
113
+ },
114
+ mainFabGlyph: {
115
+ color: m3Colors.onSecondary,
116
+ fontSize: 30,
117
+ fontFamily: fonts.bold,
118
+ marginTop: -2,
119
+ },
120
+ mainFabGlyphOpen: {
121
+ color: m3Colors.onSurface,
122
+ fontSize: 22,
123
+ },
124
+ overlay: {
125
+ flex: 1,
126
+ },
127
+ backdrop: {
128
+ ...StyleSheet.absoluteFill,
129
+ backgroundColor: m3Colors.scrim,
130
+ },
131
+ stack: {
132
+ position: 'absolute',
133
+ right: spacing.sm,
134
+ alignItems: 'flex-end',
135
+ gap: spacing.md,
136
+ },
137
+ row: {
138
+ flexDirection: 'row',
139
+ alignItems: 'center',
140
+ gap: spacing.sm,
141
+ },
142
+ labelBtn: {
143
+ paddingHorizontal: spacing.sm,
144
+ paddingVertical: spacing.xs,
145
+ },
146
+ label: {
147
+ color: m3Colors.onSurface,
148
+ fontFamily: fonts.semibold,
149
+ fontSize: 16,
150
+ },
151
+ actionFab: {
152
+ width: ACTION_FAB_SIZE,
153
+ height: ACTION_FAB_SIZE,
154
+ borderRadius: ACTION_FAB_SIZE / 2,
155
+ alignItems: 'center',
156
+ justifyContent: 'center',
157
+ ...m3Elevation.level2,
158
+ },
159
+ actionFabPrimary: {
160
+ backgroundColor: m3Colors.primary,
161
+ },
162
+ actionFabSurface: {
163
+ backgroundColor: m3Colors.inverseSurface,
164
+ },
165
+ });
@@ -0,0 +1,44 @@
1
+ export { AppButton } from './AppButton';
2
+ export { AppLoadingSpinner } from './AppLoadingSpinner';
3
+ export { AuthStepShell } from './AuthStepShell';
4
+ export { Card } from './Card';
5
+ export { ComposeAudiencePill } from './compose-audience-pill';
6
+ export {
7
+ ComposeCameraGlyph,
8
+ ComposeCloseGlyph,
9
+ ComposeFollowersGlyph,
10
+ ComposeFollowingGlyph,
11
+ ComposeGalleryGlyph,
12
+ ComposeGlobeGlyph,
13
+ ComposeLiveGlyph,
14
+ ComposeMentionGlyph,
15
+ ComposePostGlyph,
16
+ ComposeStoryGlyph,
17
+ ComposeTrashGlyph,
18
+ ComposeVideoGlyph,
19
+ } from './compose-glyphs';
20
+ export { ComposeMediaCarousel, type ComposeMediaCarouselItem } from './compose-media-carousel';
21
+ export { ComposeMediaShortcut, COMPOSE_MEDIA_TILE_SIZE } from './compose-media-shortcut';
22
+ export { ComposeRecentMediaGrid, type ComposeRecentMediaItem } from './compose-recent-media-grid';
23
+ export { ComposeScreenHeader } from './compose-screen-header';
24
+ export { ComposeTextArea, ComposeMediaPreviewFrame } from './compose-text-area';
25
+ export { ComposeToolbar, ComposeToolbarIcon, COMPOSE_TOOLBAR_ICON_SIZE } from './compose-toolbar';
26
+ export { ComposeReplyAudienceSheet, type ComposeReplyAudienceOption, type ComposeReplyAudienceValue } from './compose-reply-audience-sheet';
27
+ export { ComposeVisibilityRow } from './compose-visibility-row';
28
+ export { DateField } from './DateField';
29
+ export { FloatingCreateMenu, type FloatingCreateMenuAction } from './floating-create-menu';
30
+ export { InfoBanner } from './InfoBanner';
31
+ export { PasswordField, TextField } from './TextField';
32
+ export {
33
+ StoryCollageGlyph,
34
+ StoryEffectsGlyph,
35
+ StoryMentionGlyph,
36
+ StoryMusicGlyph,
37
+ StorySettingsGlyph,
38
+ StoryStickerGlyph,
39
+ StoryTextGlyph,
40
+ } from './story-glyphs';
41
+ export { StoryComposeHeader } from './story-compose-header';
42
+ export { StoryCreateShortcutRow, type StoryCreateShortcutItem } from './story-create-shortcut-row';
43
+ export { StoryEditorChrome, type StoryEditorTool } from './story-editor-chrome';
44
+ export { StorySurfacePicker, type StorySurfaceOption } from './story-surface-picker';
@@ -0,0 +1,72 @@
1
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
2
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
3
+ import { fonts, m3Colors, m3Type, spacing } from '../theme/proulr-theme';
4
+ import { ComposeCloseGlyph } from './compose-glyphs';
5
+ import { StorySettingsGlyph } from './story-glyphs';
6
+
7
+ export function StoryComposeHeader({
8
+ title,
9
+ onClose,
10
+ onSettings,
11
+ }: {
12
+ title: string;
13
+ onClose: () => void;
14
+ onSettings?: () => void;
15
+ }) {
16
+ const insets = useSafeAreaInsets();
17
+
18
+ return (
19
+ <View style={[styles.header, { paddingTop: insets.top + spacing.xs }]}>
20
+ <Pressable
21
+ onPress={onClose}
22
+ hitSlop={10}
23
+ accessibilityRole="button"
24
+ accessibilityLabel="Fechar"
25
+ style={styles.sideBtn}
26
+ >
27
+ <ComposeCloseGlyph />
28
+ </Pressable>
29
+
30
+ <Text style={styles.title} numberOfLines={1}>
31
+ {title}
32
+ </Text>
33
+
34
+ {onSettings ? (
35
+ <Pressable
36
+ onPress={onSettings}
37
+ hitSlop={10}
38
+ accessibilityRole="button"
39
+ accessibilityLabel="Configurações do story"
40
+ style={styles.sideBtn}
41
+ >
42
+ <StorySettingsGlyph />
43
+ </Pressable>
44
+ ) : (
45
+ <View style={styles.sideBtn} />
46
+ )}
47
+ </View>
48
+ );
49
+ }
50
+
51
+ const styles = StyleSheet.create({
52
+ header: {
53
+ flexDirection: 'row',
54
+ alignItems: 'center',
55
+ paddingHorizontal: spacing.md,
56
+ paddingBottom: spacing.sm,
57
+ backgroundColor: m3Colors.surface,
58
+ },
59
+ sideBtn: {
60
+ width: 40,
61
+ height: 40,
62
+ alignItems: 'center',
63
+ justifyContent: 'center',
64
+ },
65
+ title: {
66
+ flex: 1,
67
+ color: m3Colors.onSurface,
68
+ ...m3Type.titleSmall,
69
+ textAlign: 'center',
70
+ fontFamily: fonts.bold,
71
+ },
72
+ });
@@ -0,0 +1,89 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
3
+ import { fonts, m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
4
+
5
+ export type StoryCreateShortcutItem = {
6
+ id: string;
7
+ label: string;
8
+ badge?: string;
9
+ icon: ReactNode;
10
+ };
11
+
12
+ const SHORTCUT_WIDTH = 108;
13
+ const SHORTCUT_HEIGHT = 112;
14
+
15
+ export function StoryCreateShortcutRow({
16
+ items,
17
+ onPress,
18
+ }: {
19
+ items: StoryCreateShortcutItem[];
20
+ onPress: (id: string) => void;
21
+ }) {
22
+ return (
23
+ <ScrollView
24
+ horizontal
25
+ showsHorizontalScrollIndicator={false}
26
+ contentContainerStyle={styles.scrollContent}
27
+ >
28
+ {items.map((item) => (
29
+ <Pressable
30
+ key={item.id}
31
+ onPress={() => onPress(item.id)}
32
+ accessibilityRole="button"
33
+ accessibilityLabel={item.label}
34
+ style={({ pressed }) => [styles.card, pressed && { opacity: 0.88 }]}
35
+ >
36
+ {item.badge ? (
37
+ <View style={styles.badge}>
38
+ <Text style={styles.badgeText}>{item.badge}</Text>
39
+ </View>
40
+ ) : null}
41
+ <View style={styles.iconWrap}>{item.icon}</View>
42
+ <Text style={styles.label}>{item.label}</Text>
43
+ </Pressable>
44
+ ))}
45
+ </ScrollView>
46
+ );
47
+ }
48
+
49
+ const styles = StyleSheet.create({
50
+ scrollContent: {
51
+ paddingHorizontal: spacing.md,
52
+ gap: spacing.sm,
53
+ paddingBottom: spacing.sm,
54
+ },
55
+ card: {
56
+ width: SHORTCUT_WIDTH,
57
+ height: SHORTCUT_HEIGHT,
58
+ borderRadius: m3Shape.cornerLarge,
59
+ backgroundColor: m3Colors.surfaceContainerHigh,
60
+ borderWidth: 1,
61
+ borderColor: m3Colors.outlineVariant,
62
+ alignItems: 'center',
63
+ justifyContent: 'center',
64
+ paddingHorizontal: spacing.sm,
65
+ position: 'relative',
66
+ },
67
+ badge: {
68
+ position: 'absolute',
69
+ top: spacing.xs,
70
+ right: spacing.xs,
71
+ paddingHorizontal: spacing.xs,
72
+ paddingVertical: 2,
73
+ borderRadius: m3Shape.cornerExtraSmall,
74
+ backgroundColor: m3Colors.secondary,
75
+ },
76
+ badgeText: {
77
+ color: m3Colors.onSecondary,
78
+ fontFamily: fonts.bold,
79
+ fontSize: 10,
80
+ },
81
+ iconWrap: {
82
+ marginBottom: spacing.xs,
83
+ },
84
+ label: {
85
+ color: m3Colors.onSurface,
86
+ ...m3Type.labelSmall,
87
+ textAlign: 'center',
88
+ },
89
+ });
@@ -0,0 +1,231 @@
1
+ import type { ReactNode } from 'react';
2
+ import {
3
+ KeyboardAvoidingView,
4
+ Platform,
5
+ Pressable,
6
+ StyleSheet,
7
+ Text,
8
+ TextInput,
9
+ View,
10
+ } from 'react-native';
11
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
12
+ import { fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
13
+ import { AppLoadingSpinner } from './AppLoadingSpinner';
14
+ import { StoryBackGlyph } from './story-glyphs';
15
+
16
+ export type StoryEditorTool = {
17
+ id: string;
18
+ label: string;
19
+ icon: ReactNode;
20
+ };
21
+
22
+ export function StoryEditorChrome({
23
+ children,
24
+ tools,
25
+ caption,
26
+ onChangeCaption,
27
+ onBack,
28
+ onToolPress,
29
+ onPublish,
30
+ publishing,
31
+ publishLabel = 'Compartilhar',
32
+ footerLeft,
33
+ accessory,
34
+ showCaption = true,
35
+ mediaBackgroundColor,
36
+ }: {
37
+ children: ReactNode;
38
+ tools: StoryEditorTool[];
39
+ caption: string;
40
+ onChangeCaption: (value: string) => void;
41
+ onBack: () => void;
42
+ onToolPress: (toolId: string) => void;
43
+ onPublish: () => void;
44
+ publishing?: boolean;
45
+ publishLabel?: string;
46
+ footerLeft?: ReactNode;
47
+ accessory?: ReactNode;
48
+ showCaption?: boolean;
49
+ mediaBackgroundColor?: string;
50
+ }) {
51
+ const insets = useSafeAreaInsets();
52
+
53
+ return (
54
+ <KeyboardAvoidingView
55
+ style={[
56
+ styles.root,
57
+ mediaBackgroundColor ? { backgroundColor: mediaBackgroundColor } : null,
58
+ ]}
59
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
60
+ >
61
+ <View
62
+ style={[
63
+ styles.mediaLayer,
64
+ { backgroundColor: mediaBackgroundColor ?? m3Colors.surface },
65
+ ]}
66
+ >
67
+ {children}
68
+ </View>
69
+
70
+ <View style={[styles.topBar, { paddingTop: insets.top + spacing.xs }]}>
71
+ <Pressable
72
+ onPress={onBack}
73
+ accessibilityRole="button"
74
+ accessibilityLabel="Voltar"
75
+ style={styles.topBtn}
76
+ >
77
+ <StoryBackGlyph />
78
+ </Pressable>
79
+ </View>
80
+
81
+ <View style={[styles.bottomChrome, { paddingBottom: insets.bottom + spacing.sm }]}>
82
+ <View style={styles.toolRow}>
83
+ {tools.map((tool) => (
84
+ <Pressable
85
+ key={tool.id}
86
+ onPress={() => onToolPress(tool.id)}
87
+ accessibilityRole="button"
88
+ accessibilityLabel={tool.label}
89
+ style={styles.toolItem}
90
+ >
91
+ <View style={styles.toolIcon}>{tool.icon}</View>
92
+ <Text style={styles.toolLabel}>{tool.label}</Text>
93
+ </Pressable>
94
+ ))}
95
+ </View>
96
+
97
+ {accessory}
98
+
99
+ {showCaption ? (
100
+ <TextInput
101
+ value={caption}
102
+ onChangeText={onChangeCaption}
103
+ placeholder="Adicione uma legenda..."
104
+ placeholderTextColor={m3Colors.onSurfaceVariant}
105
+ style={styles.captionInput}
106
+ multiline
107
+ maxLength={220}
108
+ />
109
+ ) : null}
110
+
111
+ <View style={styles.footerRow}>
112
+ {footerLeft ?? <View style={styles.footerSpacer} />}
113
+ <Pressable
114
+ onPress={onPublish}
115
+ disabled={publishing}
116
+ accessibilityRole="button"
117
+ accessibilityLabel={publishLabel}
118
+ style={({ pressed }) => [
119
+ styles.publishBtn,
120
+ publishing && styles.publishBtnDisabled,
121
+ pressed && !publishing && { opacity: 0.9 },
122
+ ]}
123
+ >
124
+ {publishing ? (
125
+ <AppLoadingSpinner color={m3Colors.onPrimary} />
126
+ ) : (
127
+ <Text style={styles.publishLabel}>{publishLabel}</Text>
128
+ )}
129
+ </Pressable>
130
+ </View>
131
+ </View>
132
+ </KeyboardAvoidingView>
133
+ );
134
+ }
135
+
136
+ const styles = StyleSheet.create({
137
+ root: {
138
+ flex: 1,
139
+ backgroundColor: m3Colors.surface,
140
+ },
141
+ mediaLayer: {
142
+ ...StyleSheet.absoluteFill,
143
+ },
144
+ topBar: {
145
+ position: 'absolute',
146
+ top: 0,
147
+ left: 0,
148
+ right: 0,
149
+ paddingHorizontal: spacing.md,
150
+ zIndex: 2,
151
+ },
152
+ topBtn: {
153
+ width: 40,
154
+ height: 40,
155
+ borderRadius: 20,
156
+ backgroundColor: m3Colors.scrim,
157
+ alignItems: 'center',
158
+ justifyContent: 'center',
159
+ },
160
+ bottomChrome: {
161
+ position: 'absolute',
162
+ left: 0,
163
+ right: 0,
164
+ bottom: 0,
165
+ paddingHorizontal: spacing.md,
166
+ paddingTop: spacing.md,
167
+ backgroundColor: m3Colors.scrim,
168
+ gap: spacing.sm,
169
+ zIndex: 2,
170
+ },
171
+ toolRow: {
172
+ flexDirection: 'row',
173
+ justifyContent: 'space-between',
174
+ gap: spacing.xs,
175
+ },
176
+ toolItem: {
177
+ flex: 1,
178
+ alignItems: 'center',
179
+ gap: spacing.xs,
180
+ },
181
+ toolIcon: {
182
+ width: 44,
183
+ height: 44,
184
+ borderRadius: 22,
185
+ backgroundColor: m3Colors.surfaceContainerHigh,
186
+ alignItems: 'center',
187
+ justifyContent: 'center',
188
+ },
189
+ toolLabel: {
190
+ color: m3Colors.onPrimary,
191
+ fontFamily: fonts.medium,
192
+ fontSize: 11,
193
+ textAlign: 'center',
194
+ },
195
+ captionInput: {
196
+ color: m3Colors.onPrimary,
197
+ fontFamily: fonts.regular,
198
+ fontSize: 15,
199
+ minHeight: 40,
200
+ maxHeight: 72,
201
+ paddingHorizontal: spacing.md,
202
+ paddingVertical: spacing.sm,
203
+ borderRadius: m3Shape.cornerFull,
204
+ backgroundColor: m3Colors.surfaceContainerHigh,
205
+ },
206
+ footerRow: {
207
+ flexDirection: 'row',
208
+ alignItems: 'center',
209
+ gap: spacing.sm,
210
+ },
211
+ footerSpacer: {
212
+ flex: 1,
213
+ },
214
+ publishBtn: {
215
+ minWidth: 132,
216
+ height: 44,
217
+ borderRadius: m3Shape.cornerFull,
218
+ backgroundColor: m3Colors.primary,
219
+ alignItems: 'center',
220
+ justifyContent: 'center',
221
+ paddingHorizontal: spacing.lg,
222
+ },
223
+ publishBtnDisabled: {
224
+ opacity: 0.6,
225
+ },
226
+ publishLabel: {
227
+ color: m3Colors.onPrimary,
228
+ fontFamily: fonts.bold,
229
+ fontSize: 15,
230
+ },
231
+ });