@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.
- package/README.md +29 -0
- package/package.json +60 -0
- package/src/bdc/compile-bdc-to-wire.ts +123 -0
- package/src/bdc/components/discover.tsx +55 -0
- package/src/bdc/components/forms.tsx +20 -0
- package/src/bdc/components/native-blocks.tsx +35 -0
- package/src/bdc/components/social.tsx +27 -0
- package/src/bdc/components/ui.tsx +114 -0
- package/src/bdc/index.ts +41 -0
- package/src/bdc/jsx-runtime.ts +49 -0
- package/src/catalog/block-types.ts +18 -0
- package/src/catalog/style-tokens.ts +48 -0
- package/src/global.d.ts +1 -0
- package/src/renderer/SduiScreenRenderer.tsx +28 -0
- package/src/renderer/ads/native-ad-catalog.ts +23 -0
- package/src/renderer/blocks/SduiDiscoverBlocks.tsx +470 -0
- package/src/renderer/blocks/SduiLayoutBlocks.tsx +177 -0
- package/src/renderer/blocks/SduiNativeAdBlock.tsx +201 -0
- package/src/renderer/blocks/SduiNativeAdBlock.web.tsx +6 -0
- package/src/renderer/blocks/SduiNativeDomainBlocks.tsx +141 -0
- package/src/renderer/blocks/SduiPrimitiveBlocks.tsx +158 -0
- package/src/renderer/blocks/SduiSocialBlocks.tsx +64 -0
- package/src/renderer/index.ts +45 -0
- package/src/renderer/lib/brazilian-date-input.ts +22 -0
- package/src/renderer/lib/discover-profile-formatters.ts +75 -0
- package/src/renderer/lib/resolve-data-ref.ts +21 -0
- package/src/renderer/sdui-chrome-scroll-context.tsx +40 -0
- package/src/renderer/sdui-component-registry.tsx +129 -0
- package/src/renderer/sdui-hybrid-renderer-context.tsx +91 -0
- package/src/renderer/sdui-layout-debug.tsx +107 -0
- package/src/renderer/theme/proulr-theme.ts +164 -0
- package/src/renderer/theme/resolve-style-props.ts +85 -0
- package/src/renderer/ui/AppButton.tsx +71 -0
- package/src/renderer/ui/AppLoadingSpinner.tsx +38 -0
- package/src/renderer/ui/AuthStepShell.tsx +128 -0
- package/src/renderer/ui/Card.tsx +13 -0
- package/src/renderer/ui/DateField.tsx +36 -0
- package/src/renderer/ui/InfoBanner.tsx +51 -0
- package/src/renderer/ui/TextField.tsx +81 -0
- package/src/renderer/ui/compose-audience-pill.tsx +47 -0
- package/src/renderer/ui/compose-glyphs.tsx +400 -0
- package/src/renderer/ui/compose-media-carousel.tsx +116 -0
- package/src/renderer/ui/compose-media-shortcut.tsx +80 -0
- package/src/renderer/ui/compose-recent-media-grid.tsx +268 -0
- package/src/renderer/ui/compose-reply-audience-sheet.tsx +155 -0
- package/src/renderer/ui/compose-screen-header.tsx +115 -0
- package/src/renderer/ui/compose-text-area.tsx +94 -0
- package/src/renderer/ui/compose-toolbar-icon.tsx +45 -0
- package/src/renderer/ui/compose-toolbar.tsx +57 -0
- package/src/renderer/ui/compose-visibility-row.tsx +51 -0
- package/src/renderer/ui/floating-create-menu.tsx +165 -0
- package/src/renderer/ui/index.ts +44 -0
- package/src/renderer/ui/story-compose-header.tsx +72 -0
- package/src/renderer/ui/story-create-shortcut-row.tsx +89 -0
- package/src/renderer/ui/story-editor-chrome.tsx +231 -0
- package/src/renderer/ui/story-glyphs.tsx +144 -0
- 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,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
|
+
}
|