@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,201 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ import { LinearGradient } from 'expo-linear-gradient';
4
+ import {
5
+ DEFAULT_NATIVE_AD_ASPECT_RATIO,
6
+ DEFAULT_NATIVE_AD_PLACEMENT,
7
+ getNativeAdCatalog,
8
+ } from '../ads/native-ad-catalog';
9
+ import { colors, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
10
+ import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks';
11
+
12
+ type NativeAdModule = typeof import('react-native-google-mobile-ads');
13
+ type LoadedNativeAd = Awaited<ReturnType<NativeAdModule['NativeAd']['createForAdRequest']>>;
14
+
15
+ function resolveBlockWidth(props: Record<string, unknown>): number | undefined {
16
+ if (typeof props.width === 'number' && props.width > 0) return props.width;
17
+ return undefined;
18
+ }
19
+
20
+ function resolvePlacement(props: Record<string, unknown>): string {
21
+ return typeof props.placement === 'string' && props.placement.length > 0
22
+ ? props.placement
23
+ : DEFAULT_NATIVE_AD_PLACEMENT;
24
+ }
25
+
26
+ function resolveAspectRatio(placement: string, props: Record<string, unknown>): number {
27
+ if (typeof props.aspect_ratio === 'number' && props.aspect_ratio > 0) {
28
+ return props.aspect_ratio;
29
+ }
30
+
31
+ const catalog = getNativeAdCatalog();
32
+ const placementConfig = catalog?.getPlacementConfig?.(placement);
33
+ return placementConfig?.aspectRatio ?? DEFAULT_NATIVE_AD_ASPECT_RATIO;
34
+ }
35
+
36
+ export function SduiNativeAdBlock({ block }: SduiBlockRenderProps) {
37
+ const catalog = getNativeAdCatalog();
38
+ const placement = resolvePlacement(block.props);
39
+ const width = resolveBlockWidth(block.props);
40
+ const aspectRatio = resolveAspectRatio(placement, block.props);
41
+
42
+ const [adsModule, setAdsModule] = useState<NativeAdModule | null>(null);
43
+ const [nativeAd, setNativeAd] = useState<LoadedNativeAd | null>(null);
44
+ const [failed, setFailed] = useState(false);
45
+
46
+ useEffect(() => {
47
+ if (!catalog?.isEnabled()) return;
48
+
49
+ let cancelled = false;
50
+
51
+ void (async () => {
52
+ try {
53
+ const mod = await import('react-native-google-mobile-ads');
54
+ if (cancelled) return;
55
+
56
+ setAdsModule(mod);
57
+ const adUnitId = catalog.resolveAdUnitId(placement, mod.TestIds.NATIVE);
58
+ const ad = await mod.NativeAd.createForAdRequest(adUnitId, {
59
+ aspectRatio: mod.NativeMediaAspectRatio.PORTRAIT,
60
+ adChoicesPlacement: mod.NativeAdChoicesPlacement.TOP_RIGHT,
61
+ startVideoMuted: true,
62
+ });
63
+
64
+ if (cancelled) {
65
+ ad.destroy();
66
+ return;
67
+ }
68
+
69
+ setNativeAd(ad);
70
+ } catch {
71
+ if (!cancelled) setFailed(true);
72
+ }
73
+ })();
74
+
75
+ return () => {
76
+ cancelled = true;
77
+ };
78
+ }, [catalog, placement]);
79
+
80
+ useEffect(() => {
81
+ if (!nativeAd) return;
82
+ return () => {
83
+ nativeAd.destroy();
84
+ };
85
+ }, [nativeAd]);
86
+
87
+ if (!catalog?.isEnabled()) {
88
+ return null;
89
+ }
90
+
91
+ const cardStyle = [
92
+ styles.card,
93
+ { aspectRatio },
94
+ width != null ? { width } : styles.flexCard,
95
+ ];
96
+
97
+ if (failed) {
98
+ return <View style={[styles.placeholder, cardStyle]} />;
99
+ }
100
+
101
+ if (!adsModule || !nativeAd) {
102
+ return <View style={[styles.placeholder, styles.loading, cardStyle]} />;
103
+ }
104
+
105
+ const { NativeAdView, NativeAsset, NativeAssetType, NativeMediaView } = adsModule;
106
+
107
+ return (
108
+ <NativeAdView nativeAd={nativeAd} style={cardStyle}>
109
+ <NativeMediaView resizeMode="cover" style={styles.media} />
110
+ <LinearGradient colors={['transparent', 'rgba(0,0,0,0.85)']} style={styles.shade} />
111
+ <View style={styles.adBadge}>
112
+ <Text style={styles.adBadgeText}>Anúncio</Text>
113
+ </View>
114
+ {nativeAd.headline ? (
115
+ <NativeAsset assetType={NativeAssetType.HEADLINE}>
116
+ <Text style={styles.headline} numberOfLines={2}>
117
+ {nativeAd.headline}
118
+ </Text>
119
+ </NativeAsset>
120
+ ) : null}
121
+ {nativeAd.callToAction ? (
122
+ <NativeAsset assetType={NativeAssetType.CALL_TO_ACTION}>
123
+ <Text style={styles.cta} numberOfLines={1}>
124
+ {nativeAd.callToAction}
125
+ </Text>
126
+ </NativeAsset>
127
+ ) : null}
128
+ </NativeAdView>
129
+ );
130
+ }
131
+
132
+ const styles = StyleSheet.create({
133
+ flexCard: {
134
+ flex: 1,
135
+ },
136
+ card: {
137
+ borderRadius: m3Shape.cornerMedium,
138
+ overflow: 'hidden',
139
+ backgroundColor: m3Colors.surfaceContainerHigh,
140
+ borderWidth: 1,
141
+ borderColor: m3Colors.outline,
142
+ shadowColor: '#000',
143
+ shadowOffset: { width: 0, height: 1 },
144
+ shadowOpacity: 0.22,
145
+ shadowRadius: 2,
146
+ elevation: 1,
147
+ },
148
+ placeholder: {
149
+ borderRadius: m3Shape.cornerMedium,
150
+ backgroundColor: m3Colors.surfaceContainerHigh,
151
+ borderWidth: 1,
152
+ borderColor: m3Colors.outlineVariant,
153
+ },
154
+ loading: {
155
+ opacity: 0.55,
156
+ },
157
+ media: {
158
+ ...StyleSheet.absoluteFill,
159
+ },
160
+ shade: {
161
+ position: 'absolute',
162
+ left: 0,
163
+ right: 0,
164
+ bottom: 0,
165
+ height: 88,
166
+ },
167
+ adBadge: {
168
+ position: 'absolute',
169
+ top: spacing.sm,
170
+ left: spacing.sm,
171
+ backgroundColor: m3Colors.surfaceContainerHighest,
172
+ paddingHorizontal: spacing.sm,
173
+ paddingVertical: 3,
174
+ borderRadius: m3Shape.cornerSmall,
175
+ },
176
+ adBadgeText: {
177
+ color: colors.onSurfaceVariant,
178
+ fontSize: 10,
179
+ fontWeight: '700',
180
+ letterSpacing: 0.4,
181
+ textTransform: 'uppercase',
182
+ },
183
+ headline: {
184
+ position: 'absolute',
185
+ left: spacing.sm,
186
+ right: spacing.sm,
187
+ bottom: spacing.xl + spacing.sm,
188
+ color: m3Colors.onSurface,
189
+ fontSize: 12,
190
+ fontWeight: '700',
191
+ },
192
+ cta: {
193
+ position: 'absolute',
194
+ left: spacing.sm,
195
+ right: spacing.sm,
196
+ bottom: spacing.sm,
197
+ color: m3Colors.secondary,
198
+ fontSize: 11,
199
+ fontWeight: '600',
200
+ },
201
+ });
@@ -0,0 +1,6 @@
1
+ import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks';
2
+
3
+ /** Web stub — native ads require a native build. */
4
+ export function SduiNativeAdBlock(_props: SduiBlockRenderProps) {
5
+ return null;
6
+ }
@@ -0,0 +1,141 @@
1
+ import React from 'react';
2
+ import { Text, View } from 'react-native';
3
+ import type { SduiBlock } from '@proulr/sdui-contract';
4
+ import { useSduiHybridRenderer } from '../sdui-hybrid-renderer-context';
5
+ import { SduiLayoutDebugFrame } from '../sdui-layout-debug';
6
+ import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks';
7
+
8
+ function readStringProp(block: SduiBlock, key: string): string | undefined {
9
+ const value = block.props[key];
10
+ return typeof value === 'string' ? value : undefined;
11
+ }
12
+
13
+ function NativeBlockShell({
14
+ block,
15
+ data,
16
+ onAction,
17
+ rendererKey,
18
+ fallbackLabel,
19
+ }: SduiBlockRenderProps & {
20
+ rendererKey:
21
+ | 'renderFlashInboxList'
22
+ | 'renderChatInboxList'
23
+ | 'renderChatThread'
24
+ | 'renderSocialPostFeed'
25
+ | 'renderThoughtFeed'
26
+ | 'renderVerticalVideoPager'
27
+ | 'renderProfileView'
28
+ | 'renderProfileSelf'
29
+ | 'renderProfileSetup'
30
+ | 'renderProfileOnboarding'
31
+ | 'renderAlbumGrid'
32
+ | 'renderAlbumDetail'
33
+ | 'renderNewPostComposer'
34
+ | 'renderPostComments'
35
+ | 'renderSocialProfile'
36
+ | 'renderSettingsHome'
37
+ | 'renderDiscoverBrowseLocation';
38
+ fallbackLabel: string;
39
+ }) {
40
+ const hybrid = useSduiHybridRenderer();
41
+ const renderer = hybrid[rendererKey];
42
+ if (typeof renderer === 'function') {
43
+ return (
44
+ <SduiLayoutDebugFrame layer="native" extra={block.type} style={{ flex: 1, minHeight: 0 }}>
45
+ {renderer({
46
+ block,
47
+ data,
48
+ onAction,
49
+ dataRef: readStringProp(block, 'data_ref'),
50
+ conversationIdRef: readStringProp(block, 'conversation_id_ref'),
51
+ peerIdRef: readStringProp(block, 'peer_id_ref'),
52
+ peerNameRef: readStringProp(block, 'peer_name_ref'),
53
+ handleRef: readStringProp(block, 'handle_ref'),
54
+ postIdRef: readStringProp(block, 'post_id_ref'),
55
+ albumIdRef: readStringProp(block, 'album_id_ref'),
56
+ profileIdRef: readStringProp(block, 'profile_id_ref'),
57
+ })}
58
+ </SduiLayoutDebugFrame>
59
+ );
60
+ }
61
+
62
+ return (
63
+ <View style={{ padding: 16 }}>
64
+ <Text>{fallbackLabel}</Text>
65
+ </View>
66
+ );
67
+ }
68
+
69
+ export function SduiFlashInboxListBlock(props: SduiBlockRenderProps) {
70
+ return <NativeBlockShell {...props} rendererKey="renderFlashInboxList" fallbackLabel="Flashes" />;
71
+ }
72
+
73
+ export function SduiChatInboxListBlock(props: SduiBlockRenderProps) {
74
+ return <NativeBlockShell {...props} rendererKey="renderChatInboxList" fallbackLabel="Chat" />;
75
+ }
76
+
77
+ export function SduiChatThreadBlock(props: SduiBlockRenderProps) {
78
+ return <NativeBlockShell {...props} rendererKey="renderChatThread" fallbackLabel="Conversa" />;
79
+ }
80
+
81
+ export function SduiSocialPostFeedBlock(props: SduiBlockRenderProps) {
82
+ return <NativeBlockShell {...props} rendererKey="renderSocialPostFeed" fallbackLabel="Feed" />;
83
+ }
84
+
85
+ export function SduiThoughtFeedBlock(props: SduiBlockRenderProps) {
86
+ return <NativeBlockShell {...props} rendererKey="renderThoughtFeed" fallbackLabel="Vibes" />;
87
+ }
88
+
89
+ export function SduiVerticalVideoPagerBlock(props: SduiBlockRenderProps) {
90
+ return <NativeBlockShell {...props} rendererKey="renderVerticalVideoPager" fallbackLabel="Clips" />;
91
+ }
92
+
93
+ export function SduiProfileViewBlock(props: SduiBlockRenderProps) {
94
+ return <NativeBlockShell {...props} rendererKey="renderProfileView" fallbackLabel="Perfil" />;
95
+ }
96
+
97
+ export function SduiProfileSelfBlock(props: SduiBlockRenderProps) {
98
+ return <NativeBlockShell {...props} rendererKey="renderProfileSelf" fallbackLabel="Meu perfil" />;
99
+ }
100
+
101
+ export function SduiProfileSetupBlock(props: SduiBlockRenderProps) {
102
+ return <NativeBlockShell {...props} rendererKey="renderProfileSetup" fallbackLabel="Editar perfil" />;
103
+ }
104
+
105
+ export function SduiProfileOnboardingBlock(props: SduiBlockRenderProps) {
106
+ return <NativeBlockShell {...props} rendererKey="renderProfileOnboarding" fallbackLabel="Onboarding" />;
107
+ }
108
+
109
+ export function SduiAlbumGridBlock(props: SduiBlockRenderProps) {
110
+ return <NativeBlockShell {...props} rendererKey="renderAlbumGrid" fallbackLabel="Álbuns" />;
111
+ }
112
+
113
+ export function SduiAlbumDetailBlock(props: SduiBlockRenderProps) {
114
+ return <NativeBlockShell {...props} rendererKey="renderAlbumDetail" fallbackLabel="Álbum" />;
115
+ }
116
+
117
+ export function SduiNewPostComposerBlock(props: SduiBlockRenderProps) {
118
+ return <NativeBlockShell {...props} rendererKey="renderNewPostComposer" fallbackLabel="Nova publicação" />;
119
+ }
120
+
121
+ export function SduiPostCommentsBlock(props: SduiBlockRenderProps) {
122
+ return <NativeBlockShell {...props} rendererKey="renderPostComments" fallbackLabel="Comentários" />;
123
+ }
124
+
125
+ export function SduiSocialProfileBlock(props: SduiBlockRenderProps) {
126
+ return <NativeBlockShell {...props} rendererKey="renderSocialProfile" fallbackLabel="Perfil social" />;
127
+ }
128
+
129
+ export function SduiSettingsHomeBlock(props: SduiBlockRenderProps) {
130
+ return <NativeBlockShell {...props} rendererKey="renderSettingsHome" fallbackLabel="Configurações" />;
131
+ }
132
+
133
+ export function SduiDiscoverBrowseLocationBlock(props: SduiBlockRenderProps) {
134
+ return (
135
+ <NativeBlockShell
136
+ {...props}
137
+ rendererKey="renderDiscoverBrowseLocation"
138
+ fallbackLabel="Caçar"
139
+ />
140
+ );
141
+ }
@@ -0,0 +1,158 @@
1
+ import React from 'react';
2
+ import { ScrollView, StyleSheet, Text, View } from 'react-native';
3
+ import type { SduiBlock } from '@proulr/sdui-contract';
4
+ import { AppButton } from '../ui/AppButton';
5
+ import { colors, m3Type, spacing } from '../theme/proulr-theme';
6
+ import {
7
+ resolveGapProp,
8
+ resolvePaddingProp,
9
+ resolveTextStyleProps,
10
+ resolveViewStyleProps,
11
+ } from '../theme/resolve-style-props';
12
+ import { SduiLayoutDebugFrame } from '../sdui-layout-debug';
13
+ import { useSduiChromeScroll } from '../sdui-chrome-scroll-context';
14
+
15
+ export type SduiBlockRenderProps = {
16
+ block: SduiBlock;
17
+ data: Record<string, unknown>;
18
+ onAction: (actionId: string, vars?: Record<string, string>) => void;
19
+ children?: React.ReactNode;
20
+ };
21
+
22
+ const textVariants = ['headline', 'title', 'body', 'label'] as const;
23
+
24
+ export function SduiTextBlock({ block }: SduiBlockRenderProps) {
25
+ const text = typeof block.props.text === 'string' ? block.props.text : '';
26
+ const variant =
27
+ typeof block.props.variant === 'string' &&
28
+ (textVariants as readonly string[]).includes(block.props.variant)
29
+ ? block.props.variant
30
+ : 'body';
31
+
32
+ return (
33
+ <Text
34
+ style={[
35
+ { color: colors.onSurface },
36
+ resolveTextStyleProps(block.props.style as Record<string, unknown>, variant),
37
+ ]}
38
+ >
39
+ {text}
40
+ </Text>
41
+ );
42
+ }
43
+
44
+ export function SduiColumnBlock({ block, children }: SduiBlockRenderProps) {
45
+ const gap = resolveGapProp(block.props.gap, spacing.sm);
46
+ const padding = resolvePaddingProp(block.props.padding, 0);
47
+ const fillHeight = gap === 0 && padding === 0;
48
+ const childList = React.Children.toArray(children);
49
+ const singleFillChild = fillHeight && childList.length === 1;
50
+
51
+ return (
52
+ <SduiLayoutDebugFrame
53
+ layer="column"
54
+ extra={singleFillChild ? 'fill×1' : `children:${childList.length}`}
55
+ style={[
56
+ { gap, padding, ...(fillHeight ? { flex: 1, minHeight: 0, alignSelf: 'stretch' } : null) },
57
+ resolveViewStyleProps(block.props.style as Record<string, unknown>),
58
+ ]}
59
+ >
60
+ {singleFillChild ? (
61
+ <View style={{ flex: 1, minHeight: 0, alignSelf: 'stretch' }}>{childList[0]}</View>
62
+ ) : (
63
+ children
64
+ )}
65
+ </SduiLayoutDebugFrame>
66
+ );
67
+ }
68
+
69
+ export function SduiRowBlock({ block, children }: SduiBlockRenderProps) {
70
+ const gap = resolveGapProp(block.props.gap, spacing.sm);
71
+ return (
72
+ <View
73
+ style={[
74
+ { flexDirection: 'row', gap },
75
+ resolveViewStyleProps(block.props.style as Record<string, unknown>),
76
+ ]}
77
+ >
78
+ {children}
79
+ </View>
80
+ );
81
+ }
82
+
83
+ export function SduiScrollBlock({ block, children }: SduiBlockRenderProps) {
84
+ const chromeScroll = useSduiChromeScroll();
85
+ const padding = resolvePaddingProp(block.props.padding, spacing.sm);
86
+ const gap = resolveGapProp(block.props.gap, spacing.md);
87
+ const blockContentStyle = chromeScroll
88
+ ? { paddingHorizontal: padding, gap }
89
+ : { padding, gap };
90
+
91
+ return (
92
+ <SduiLayoutDebugFrame layer="scroll" style={{ flex: 1, minHeight: 0, alignSelf: 'stretch' }}>
93
+ <ScrollView
94
+ style={[styles.scroll, resolveViewStyleProps(block.props.style as Record<string, unknown>)]}
95
+ contentContainerStyle={
96
+ chromeScroll
97
+ ? [chromeScroll.contentContainerStyle, blockContentStyle]
98
+ : blockContentStyle
99
+ }
100
+ {...(chromeScroll?.scrollInsetProps ?? {})}
101
+ showsVerticalScrollIndicator={false}
102
+ >
103
+ {children}
104
+ </ScrollView>
105
+ </SduiLayoutDebugFrame>
106
+ );
107
+ }
108
+
109
+ export function SduiButtonBlock({ block, onAction }: SduiBlockRenderProps) {
110
+ const label = typeof block.props.label === 'string' ? block.props.label : 'Ação';
111
+ const actionId = typeof block.props.action_id === 'string' ? block.props.action_id : '';
112
+ const variant =
113
+ typeof block.props.variant === 'string'
114
+ ? (block.props.variant as 'primary' | 'secondary' | 'ghost' | 'outline' | 'danger')
115
+ : 'primary';
116
+
117
+ return (
118
+ <View style={resolveViewStyleProps(block.props.style as Record<string, unknown>)}>
119
+ <AppButton
120
+ label={label}
121
+ variant={variant}
122
+ onPress={() => actionId && onAction(actionId)}
123
+ />
124
+ </View>
125
+ );
126
+ }
127
+
128
+ export function SduiSpacerBlock({ block }: SduiBlockRenderProps) {
129
+ const height = typeof block.props.height === 'number' ? block.props.height : spacing.md;
130
+ return <View style={{ height }} />;
131
+ }
132
+
133
+ export function SduiInfoBannerBlock({ block }: SduiBlockRenderProps) {
134
+ const message = typeof block.props.message === 'string' ? block.props.message : '';
135
+ return (
136
+ <View style={[styles.banner, resolveViewStyleProps(block.props.style as Record<string, unknown>)]}>
137
+ <Text style={styles.bannerText}>{message}</Text>
138
+ </View>
139
+ );
140
+ }
141
+
142
+ export function SduiUnknownBlock({ block }: SduiBlockRenderProps) {
143
+ if (!__DEV__) return null;
144
+ return <Text style={styles.unknown}>Bloco desconhecido: {block.type}</Text>;
145
+ }
146
+
147
+ const styles = StyleSheet.create({
148
+ scroll: { flex: 1, minHeight: 0 },
149
+ banner: {
150
+ padding: spacing.md,
151
+ backgroundColor: colors.surface,
152
+ borderRadius: spacing.sm,
153
+ borderWidth: 1,
154
+ borderColor: colors.border,
155
+ },
156
+ bannerText: { ...m3Type.body, color: colors.onSurfaceVariant },
157
+ unknown: { ...m3Type.label, color: colors.onSurfaceVariant, padding: spacing.md },
158
+ });
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ import type { SduiBlock } from '@proulr/sdui-contract';
4
+ import { resolveDataRefString } from '../lib/resolve-data-ref';
5
+ import { useSduiHybridRenderer } from '../sdui-hybrid-renderer-context';
6
+ import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks';
7
+
8
+ function readStringProp(block: SduiBlock, key: string): string | undefined {
9
+ const value = block.props[key];
10
+ return typeof value === 'string' ? value : undefined;
11
+ }
12
+
13
+ export function SduiStoryRailBlock({ block }: SduiBlockRenderProps) {
14
+ const hybrid = useSduiHybridRenderer();
15
+ const surface = readStringProp(block, 'surface') === 'vibes' ? 'vibes' : 'discover';
16
+
17
+ if (hybrid.renderStoryRail) {
18
+ return <>{hybrid.renderStoryRail({ surface })}</>;
19
+ }
20
+
21
+ return (
22
+ <View style={styles.placeholder}>
23
+ <Text style={styles.placeholderText}>StoryRail ({surface})</Text>
24
+ </View>
25
+ );
26
+ }
27
+
28
+ export function SduiProfileCompletionBannerBlock({ block, data, onAction }: SduiBlockRenderProps) {
29
+ const hybrid = useSduiHybridRenderer();
30
+ const percentRef = readStringProp(block, 'percent_ref') ?? 'profile_completion_percent';
31
+ const percent = Number(resolveDataRefString(data, percentRef) || '0');
32
+ const dismissActionId = readStringProp(block, 'dismiss_action_id');
33
+ const completeActionId = readStringProp(block, 'complete_action_id');
34
+
35
+ if (hybrid.renderProfileCompletionBanner) {
36
+ return (
37
+ <>
38
+ {hybrid.renderProfileCompletionBanner({
39
+ percent,
40
+ onDismiss: () => dismissActionId && onAction(dismissActionId),
41
+ onComplete: () => completeActionId && onAction(completeActionId),
42
+ })}
43
+ </>
44
+ );
45
+ }
46
+
47
+ if (percent <= 0) return null;
48
+
49
+ return (
50
+ <View style={styles.placeholder}>
51
+ <Text style={styles.placeholderText}>Complete seu perfil ({percent}%)</Text>
52
+ </View>
53
+ );
54
+ }
55
+
56
+ const styles = StyleSheet.create({
57
+ placeholder: {
58
+ padding: 12,
59
+ },
60
+ placeholderText: {
61
+ color: '#A3A3A3',
62
+ fontSize: 13,
63
+ },
64
+ });
@@ -0,0 +1,45 @@
1
+ export { SduiScreenRenderer } from './SduiScreenRenderer';
2
+ export {
3
+ resolveSduiBlockComponent,
4
+ registerSduiBlockComponent,
5
+ renderBlockTree,
6
+ } from './sdui-component-registry';
7
+ export type { SduiBlockRenderProps } from './blocks/SduiPrimitiveBlocks';
8
+ export {
9
+ SduiHybridRendererProvider,
10
+ useSduiHybridRenderer,
11
+ } from './sdui-hybrid-renderer-context';
12
+ export {
13
+ SduiChromeScrollProvider,
14
+ useSduiChromeScroll,
15
+ } from './sdui-chrome-scroll-context';
16
+ export type {
17
+ SduiChromeScrollConfig,
18
+ SduiChromeScrollInsetProps,
19
+ SduiChromeScrollInsets,
20
+ } from './sdui-chrome-scroll-context';
21
+ export type {
22
+ SduiHybridDiscoverMapProps,
23
+ SduiHybridDiscoverProfileCardProps,
24
+ SduiHybridProfileCompletionBannerProps,
25
+ SduiHybridRenderers,
26
+ SduiHybridStoryRailProps,
27
+ SduiNativeBlockRenderProps,
28
+ } from './sdui-hybrid-renderer-context';
29
+ export * from './theme/proulr-theme';
30
+ export { resolveViewStyleProps, resolveTextStyleProps } from './theme/resolve-style-props';
31
+ export {
32
+ formatDistanceLabel,
33
+ getPresenceStatus,
34
+ profileAgeFromBirthDate,
35
+ } from './lib/discover-profile-formatters';
36
+ export type { DiscoverNearbyProfileWire } from './lib/discover-profile-formatters';
37
+ export {
38
+ configureNativeAdCatalog,
39
+ getNativeAdCatalog,
40
+ DEFAULT_NATIVE_AD_PLACEMENT,
41
+ DEFAULT_NATIVE_AD_ASPECT_RATIO,
42
+ } from './ads/native-ad-catalog';
43
+ export type { NativeAdCatalog, NativeAdPlacementConfig } from './ads/native-ad-catalog';
44
+ export * from './ui';
45
+ export { brToISO, isValidDateBR, maskDateBR } from './lib/brazilian-date-input';
@@ -0,0 +1,22 @@
1
+ export function maskDateBR(input: string) {
2
+ const digits = input.replace(/\D/g, '').slice(0, 8);
3
+ const parts: string[] = [];
4
+ if (digits.length > 0) parts.push(digits.slice(0, 2));
5
+ if (digits.length > 2) parts.push(digits.slice(2, 4));
6
+ if (digits.length > 4) parts.push(digits.slice(4, 8));
7
+ return parts.join('/');
8
+ }
9
+
10
+ export function isValidDateBR(value: string) {
11
+ const m = value.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
12
+ if (!m) return false;
13
+ const [, dd, mm, yyyy] = m.map(Number) as unknown as [string, number, number, number];
14
+ const d = new Date(yyyy, mm - 1, dd);
15
+ return d.getFullYear() === yyyy && d.getMonth() === mm - 1 && d.getDate() === dd;
16
+ }
17
+
18
+ export function brToISO(value: string): string | null {
19
+ if (!isValidDateBR(value)) return null;
20
+ const [dd, mm, yyyy] = value.split('/');
21
+ return `${yyyy}-${mm}-${dd}`;
22
+ }