@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,85 @@
|
|
|
1
|
+
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
|
2
|
+
import type { ColorToken, ShapeToken, SpacingToken, StyleWire, TypeToken } from '@proulr/sdui-contract';
|
|
3
|
+
import { m3Colors, m3Shape, m3Type, spacing } from './proulr-theme';
|
|
4
|
+
|
|
5
|
+
function resolveColor(token: ColorToken | undefined): string | undefined {
|
|
6
|
+
if (!token) return undefined;
|
|
7
|
+
return m3Colors[token as keyof typeof m3Colors];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function resolveSpacing(token: SpacingToken | number | undefined): number | undefined {
|
|
11
|
+
if (token == null) return undefined;
|
|
12
|
+
if (typeof token === 'number') return token;
|
|
13
|
+
return spacing[token];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveShape(token: ShapeToken | number | undefined): number | undefined {
|
|
17
|
+
if (token == null) return undefined;
|
|
18
|
+
if (typeof token === 'number') return token;
|
|
19
|
+
return m3Shape[token];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveTypography(token: TypeToken | undefined): TextStyle | undefined {
|
|
23
|
+
if (!token) return undefined;
|
|
24
|
+
return m3Type[token];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function snakeToCamel(key: string): string {
|
|
28
|
+
return key.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeStyleWire(style: StyleWire | Record<string, unknown> | undefined): StyleWire {
|
|
32
|
+
if (!style) return {};
|
|
33
|
+
const normalized: StyleWire = {};
|
|
34
|
+
for (const [key, value] of Object.entries(style)) {
|
|
35
|
+
const camelKey = snakeToCamel(key) as keyof StyleWire;
|
|
36
|
+
(normalized as Record<string, unknown>)[camelKey] = value;
|
|
37
|
+
}
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveViewStyleProps(
|
|
42
|
+
styleWire: StyleWire | Record<string, unknown> | undefined,
|
|
43
|
+
): StyleProp<ViewStyle> {
|
|
44
|
+
const style = normalizeStyleWire(styleWire);
|
|
45
|
+
return {
|
|
46
|
+
backgroundColor: resolveColor(style.background_color),
|
|
47
|
+
padding: resolveSpacing(style.padding),
|
|
48
|
+
paddingHorizontal: resolveSpacing(style.padding_horizontal),
|
|
49
|
+
paddingVertical: resolveSpacing(style.padding_vertical),
|
|
50
|
+
marginTop: resolveSpacing(style.margin_top),
|
|
51
|
+
gap: resolveSpacing(style.gap),
|
|
52
|
+
borderRadius: resolveShape(style.border_radius),
|
|
53
|
+
borderWidth: style.border_width,
|
|
54
|
+
borderColor: resolveColor(style.border_color),
|
|
55
|
+
opacity: style.opacity,
|
|
56
|
+
flex: style.flex,
|
|
57
|
+
alignItems: style.align_items,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveTextStyleProps(
|
|
62
|
+
styleWire: StyleWire | Record<string, unknown> | undefined,
|
|
63
|
+
variant?: string,
|
|
64
|
+
): StyleProp<TextStyle> {
|
|
65
|
+
const style = normalizeStyleWire(styleWire);
|
|
66
|
+
const variantType = (style.typography ?? variant) as TypeToken | undefined;
|
|
67
|
+
return {
|
|
68
|
+
...resolveTypography(variantType),
|
|
69
|
+
color: resolveColor(style.color),
|
|
70
|
+
marginTop: resolveSpacing(style.margin_top),
|
|
71
|
+
opacity: style.opacity,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function resolveGapProp(gap: unknown, fallback: number): number {
|
|
76
|
+
if (typeof gap === 'number') return gap;
|
|
77
|
+
if (typeof gap === 'string' && gap in spacing) return spacing[gap as SpacingToken];
|
|
78
|
+
return fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resolvePaddingProp(padding: unknown, fallback = 0): number {
|
|
82
|
+
if (typeof padding === 'number') return padding;
|
|
83
|
+
if (typeof padding === 'string' && padding in spacing) return spacing[padding as SpacingToken];
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Pressable, StyleSheet, Text } from 'react-native';
|
|
2
|
+
import { fonts, m3Colors, m3Elevation, m3Shape, spacing } from '../theme/proulr-theme';
|
|
3
|
+
import { AppLoadingSpinner } from './AppLoadingSpinner';
|
|
4
|
+
|
|
5
|
+
export function AppButton({
|
|
6
|
+
label,
|
|
7
|
+
onPress,
|
|
8
|
+
variant = 'primary',
|
|
9
|
+
disabled,
|
|
10
|
+
loading,
|
|
11
|
+
compact,
|
|
12
|
+
}: {
|
|
13
|
+
label: string;
|
|
14
|
+
onPress: () => void;
|
|
15
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'outline' | 'danger';
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
compact?: boolean;
|
|
19
|
+
}) {
|
|
20
|
+
const isPrimary = variant === 'primary';
|
|
21
|
+
const isSecondary = variant === 'secondary';
|
|
22
|
+
const isOutline = variant === 'outline';
|
|
23
|
+
const isDanger = variant === 'danger';
|
|
24
|
+
const labelColor =
|
|
25
|
+
isPrimary || isDanger || isSecondary
|
|
26
|
+
? m3Colors.onPrimary
|
|
27
|
+
: isOutline || variant === 'ghost'
|
|
28
|
+
? m3Colors.primary
|
|
29
|
+
: m3Colors.onPrimary;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Pressable
|
|
33
|
+
onPress={onPress}
|
|
34
|
+
disabled={disabled || loading}
|
|
35
|
+
style={({ pressed }) => [
|
|
36
|
+
styles.btn,
|
|
37
|
+
compact && styles.btnCompact,
|
|
38
|
+
isPrimary && { backgroundColor: m3Colors.primary },
|
|
39
|
+
isSecondary && { backgroundColor: m3Colors.surfaceContainerHigh },
|
|
40
|
+
isDanger && { backgroundColor: m3Colors.error },
|
|
41
|
+
isOutline && {
|
|
42
|
+
backgroundColor: 'transparent',
|
|
43
|
+
borderWidth: 1,
|
|
44
|
+
borderColor: m3Colors.outline,
|
|
45
|
+
},
|
|
46
|
+
variant === 'ghost' && { backgroundColor: 'transparent' },
|
|
47
|
+
(disabled || loading) && { opacity: 0.38 },
|
|
48
|
+
pressed && { opacity: 0.85 },
|
|
49
|
+
(isPrimary || isSecondary) && m3Elevation.level1,
|
|
50
|
+
]}
|
|
51
|
+
>
|
|
52
|
+
{loading ? (
|
|
53
|
+
<AppLoadingSpinner color={labelColor} />
|
|
54
|
+
) : (
|
|
55
|
+
<Text style={[styles.btnText, { color: labelColor }]}>{label}</Text>
|
|
56
|
+
)}
|
|
57
|
+
</Pressable>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const styles = StyleSheet.create({
|
|
62
|
+
btn: {
|
|
63
|
+
height: 48,
|
|
64
|
+
borderRadius: m3Shape.cornerFull,
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
justifyContent: 'center',
|
|
67
|
+
paddingHorizontal: spacing.xl,
|
|
68
|
+
},
|
|
69
|
+
btnCompact: { height: 40, paddingHorizontal: spacing.sm },
|
|
70
|
+
btnText: { fontSize: 14, fontFamily: fonts.medium, letterSpacing: 0.1 },
|
|
71
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ActivityIndicator, StyleSheet, View, ViewStyle } from 'react-native';
|
|
3
|
+
import { m3Colors } from '../theme/proulr-theme';
|
|
4
|
+
|
|
5
|
+
export const loadingIndicatorSize = 'large' as const;
|
|
6
|
+
export const loadingIndicatorColor = m3Colors.primary;
|
|
7
|
+
|
|
8
|
+
type LoadingIndicatorProps = {
|
|
9
|
+
fullScreen?: boolean;
|
|
10
|
+
color?: string;
|
|
11
|
+
style?: ViewStyle;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function AppLoadingSpinner({
|
|
15
|
+
fullScreen = false,
|
|
16
|
+
color = loadingIndicatorColor,
|
|
17
|
+
style,
|
|
18
|
+
}: LoadingIndicatorProps) {
|
|
19
|
+
const spinner = <ActivityIndicator color={color} size={loadingIndicatorSize} />;
|
|
20
|
+
|
|
21
|
+
if (fullScreen) {
|
|
22
|
+
return <View style={[styles.fullScreen, style]}>{spinner}</View>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (style) {
|
|
26
|
+
return <View style={style}>{spinner}</View>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return spinner;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const styles = StyleSheet.create({
|
|
33
|
+
fullScreen: {
|
|
34
|
+
flex: 1,
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
justifyContent: 'center',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
KeyboardAvoidingView,
|
|
4
|
+
Platform,
|
|
5
|
+
Pressable,
|
|
6
|
+
ScrollView,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Text,
|
|
9
|
+
View,
|
|
10
|
+
type ViewStyle,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
13
|
+
import { colors, fonts, m3Colors, m3Type, spacing } from '../theme/proulr-theme';
|
|
14
|
+
|
|
15
|
+
type AuthStepShellProps = {
|
|
16
|
+
title: string;
|
|
17
|
+
subtitle?: string;
|
|
18
|
+
onBack?: () => void;
|
|
19
|
+
backLabel?: string;
|
|
20
|
+
headerSlot?: React.ReactNode;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
contentStyle?: ViewStyle;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Shell de auth/onboarding — título inline, sem chrome pill. */
|
|
26
|
+
export function AuthStepShell({
|
|
27
|
+
title,
|
|
28
|
+
subtitle,
|
|
29
|
+
onBack,
|
|
30
|
+
backLabel = 'Voltar',
|
|
31
|
+
headerSlot,
|
|
32
|
+
children,
|
|
33
|
+
contentStyle,
|
|
34
|
+
}: AuthStepShellProps) {
|
|
35
|
+
const insets = useSafeAreaInsets();
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<KeyboardAvoidingView
|
|
39
|
+
style={styles.root}
|
|
40
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
41
|
+
>
|
|
42
|
+
<View style={[styles.inner, { paddingTop: insets.top + spacing.md }]}>
|
|
43
|
+
{onBack ? (
|
|
44
|
+
<Pressable
|
|
45
|
+
accessibilityRole="button"
|
|
46
|
+
onPress={onBack}
|
|
47
|
+
style={({ pressed }) => [styles.backBtn, pressed && styles.pressed]}
|
|
48
|
+
>
|
|
49
|
+
<Text style={styles.backGlyph}>{'\u2039'}</Text>
|
|
50
|
+
<Text style={styles.backLabel}>{backLabel}</Text>
|
|
51
|
+
</Pressable>
|
|
52
|
+
) : null}
|
|
53
|
+
|
|
54
|
+
{headerSlot ? <View style={styles.headerSlot}>{headerSlot}</View> : null}
|
|
55
|
+
|
|
56
|
+
<Text style={styles.title}>{title}</Text>
|
|
57
|
+
{subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : null}
|
|
58
|
+
|
|
59
|
+
<ScrollView
|
|
60
|
+
style={styles.scroll}
|
|
61
|
+
contentContainerStyle={[
|
|
62
|
+
styles.scrollContent,
|
|
63
|
+
{ paddingBottom: insets.bottom + spacing.xxl },
|
|
64
|
+
contentStyle,
|
|
65
|
+
]}
|
|
66
|
+
keyboardShouldPersistTaps="handled"
|
|
67
|
+
showsVerticalScrollIndicator={false}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</ScrollView>
|
|
71
|
+
</View>
|
|
72
|
+
</KeyboardAvoidingView>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const styles = StyleSheet.create({
|
|
77
|
+
root: {
|
|
78
|
+
flex: 1,
|
|
79
|
+
backgroundColor: m3Colors.surface,
|
|
80
|
+
},
|
|
81
|
+
inner: {
|
|
82
|
+
flex: 1,
|
|
83
|
+
paddingHorizontal: spacing.xl,
|
|
84
|
+
},
|
|
85
|
+
backBtn: {
|
|
86
|
+
flexDirection: 'row',
|
|
87
|
+
alignItems: 'center',
|
|
88
|
+
alignSelf: 'flex-start',
|
|
89
|
+
gap: spacing.xs,
|
|
90
|
+
marginBottom: spacing.md,
|
|
91
|
+
paddingVertical: spacing.xs,
|
|
92
|
+
},
|
|
93
|
+
backGlyph: {
|
|
94
|
+
color: m3Colors.onSurface,
|
|
95
|
+
fontSize: 24,
|
|
96
|
+
fontFamily: fonts.bold,
|
|
97
|
+
marginTop: -2,
|
|
98
|
+
},
|
|
99
|
+
backLabel: {
|
|
100
|
+
color: colors.textMuted,
|
|
101
|
+
fontSize: 14,
|
|
102
|
+
fontFamily: fonts.medium,
|
|
103
|
+
},
|
|
104
|
+
pressed: {
|
|
105
|
+
opacity: 0.6,
|
|
106
|
+
},
|
|
107
|
+
headerSlot: {
|
|
108
|
+
alignItems: 'center',
|
|
109
|
+
marginBottom: spacing.lg,
|
|
110
|
+
},
|
|
111
|
+
title: {
|
|
112
|
+
...m3Type.headline,
|
|
113
|
+
fontFamily: fonts.extrabold,
|
|
114
|
+
color: m3Colors.onSurface,
|
|
115
|
+
},
|
|
116
|
+
subtitle: {
|
|
117
|
+
...m3Type.body,
|
|
118
|
+
color: m3Colors.onSurfaceVariant,
|
|
119
|
+
marginTop: spacing.sm,
|
|
120
|
+
},
|
|
121
|
+
scroll: {
|
|
122
|
+
flex: 1,
|
|
123
|
+
marginTop: spacing.xl,
|
|
124
|
+
},
|
|
125
|
+
scrollContent: {
|
|
126
|
+
gap: spacing.sm,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet, View, ViewStyle } from 'react-native';
|
|
3
|
+
import { cardStyle } from '../theme/proulr-theme';
|
|
4
|
+
|
|
5
|
+
export function Card({ children, style }: { children: React.ReactNode; style?: ViewStyle }) {
|
|
6
|
+
return <View style={[styles.card, style]}>{children}</View>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const styles = StyleSheet.create({
|
|
10
|
+
card: {
|
|
11
|
+
...cardStyle,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TextInput, TextInputProps, ViewStyle } from 'react-native';
|
|
3
|
+
import { maskDateBR } from '../lib/brazilian-date-input';
|
|
4
|
+
import { TextField } from './TextField';
|
|
5
|
+
|
|
6
|
+
export function DateField({
|
|
7
|
+
value,
|
|
8
|
+
onChange,
|
|
9
|
+
placeholder = 'DD/MM/AAAA',
|
|
10
|
+
containerStyle,
|
|
11
|
+
inputRef,
|
|
12
|
+
returnKeyType,
|
|
13
|
+
onSubmitEditing,
|
|
14
|
+
}: {
|
|
15
|
+
value: string;
|
|
16
|
+
onChange: (masked: string) => void;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
containerStyle?: ViewStyle;
|
|
19
|
+
inputRef?: React.Ref<TextInput>;
|
|
20
|
+
returnKeyType?: TextInputProps['returnKeyType'];
|
|
21
|
+
onSubmitEditing?: TextInputProps['onSubmitEditing'];
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<TextField
|
|
25
|
+
inputRef={inputRef}
|
|
26
|
+
placeholder={placeholder}
|
|
27
|
+
keyboardType="number-pad"
|
|
28
|
+
maxLength={10}
|
|
29
|
+
value={value}
|
|
30
|
+
returnKeyType={returnKeyType}
|
|
31
|
+
onSubmitEditing={onSubmitEditing}
|
|
32
|
+
onChangeText={(t) => onChange(maskDateBR(t))}
|
|
33
|
+
containerStyle={containerStyle}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import { colors, fonts, radius, spacing } from '../theme/proulr-theme';
|
|
4
|
+
|
|
5
|
+
export function InfoBanner({
|
|
6
|
+
message,
|
|
7
|
+
icon = 'ℹ',
|
|
8
|
+
onDismiss,
|
|
9
|
+
}: {
|
|
10
|
+
message: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
onDismiss?: () => void;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<View style={styles.infoBanner}>
|
|
16
|
+
<View style={styles.infoIconWrap}>
|
|
17
|
+
<Text style={styles.infoIcon}>{icon}</Text>
|
|
18
|
+
</View>
|
|
19
|
+
<Text style={styles.infoText}>{message}</Text>
|
|
20
|
+
{onDismiss && (
|
|
21
|
+
<Pressable onPress={onDismiss} hitSlop={8}>
|
|
22
|
+
<Text style={styles.infoDismiss}>✕</Text>
|
|
23
|
+
</Pressable>
|
|
24
|
+
)}
|
|
25
|
+
</View>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const styles = StyleSheet.create({
|
|
30
|
+
infoBanner: {
|
|
31
|
+
flexDirection: 'row',
|
|
32
|
+
alignItems: 'center',
|
|
33
|
+
gap: spacing.md,
|
|
34
|
+
backgroundColor: colors.accentSoft,
|
|
35
|
+
borderRadius: radius.lg,
|
|
36
|
+
padding: spacing.md,
|
|
37
|
+
borderWidth: 1,
|
|
38
|
+
borderColor: colors.border,
|
|
39
|
+
},
|
|
40
|
+
infoIconWrap: {
|
|
41
|
+
width: 36,
|
|
42
|
+
height: 36,
|
|
43
|
+
borderRadius: 18,
|
|
44
|
+
backgroundColor: colors.surface2,
|
|
45
|
+
alignItems: 'center',
|
|
46
|
+
justifyContent: 'center',
|
|
47
|
+
},
|
|
48
|
+
infoIcon: { fontSize: 16, color: colors.accentText },
|
|
49
|
+
infoText: { flex: 1, color: colors.textSecondary, fontSize: 13, lineHeight: 18, fontFamily: fonts.medium },
|
|
50
|
+
infoDismiss: { color: colors.textMuted, fontSize: 16 },
|
|
51
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, Text, TextInput, TextInputProps, View, ViewStyle } from 'react-native';
|
|
3
|
+
import { fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
|
|
4
|
+
|
|
5
|
+
export function TextField(
|
|
6
|
+
props: TextInputProps & {
|
|
7
|
+
containerStyle?: ViewStyle;
|
|
8
|
+
focused?: boolean;
|
|
9
|
+
inputRef?: React.Ref<TextInput>;
|
|
10
|
+
},
|
|
11
|
+
) {
|
|
12
|
+
const { containerStyle, focused, inputRef, ...rest } = props;
|
|
13
|
+
return (
|
|
14
|
+
<View
|
|
15
|
+
style={[
|
|
16
|
+
styles.field,
|
|
17
|
+
focused && { borderColor: m3Colors.primary, borderWidth: 2 },
|
|
18
|
+
containerStyle,
|
|
19
|
+
]}
|
|
20
|
+
>
|
|
21
|
+
<TextInput
|
|
22
|
+
ref={inputRef}
|
|
23
|
+
placeholderTextColor={m3Colors.onSurfaceVariant}
|
|
24
|
+
style={styles.fieldInput}
|
|
25
|
+
{...rest}
|
|
26
|
+
/>
|
|
27
|
+
</View>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function PasswordField(
|
|
32
|
+
props: TextInputProps & {
|
|
33
|
+
containerStyle?: ViewStyle;
|
|
34
|
+
inputRef?: React.Ref<TextInput>;
|
|
35
|
+
},
|
|
36
|
+
) {
|
|
37
|
+
const { containerStyle, inputRef, ...rest } = props;
|
|
38
|
+
const [visible, setVisible] = useState(false);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View style={[styles.field, styles.passwordField, containerStyle]}>
|
|
42
|
+
<TextInput
|
|
43
|
+
ref={inputRef}
|
|
44
|
+
placeholderTextColor={m3Colors.onSurfaceVariant}
|
|
45
|
+
style={[styles.fieldInput, styles.passwordInput]}
|
|
46
|
+
secureTextEntry={!visible}
|
|
47
|
+
{...rest}
|
|
48
|
+
/>
|
|
49
|
+
<Pressable
|
|
50
|
+
onPress={() => setVisible((v) => !v)}
|
|
51
|
+
hitSlop={8}
|
|
52
|
+
style={styles.passwordToggle}
|
|
53
|
+
accessibilityLabel={visible ? 'Ocultar senha' : 'Mostrar senha'}
|
|
54
|
+
>
|
|
55
|
+
<Text style={styles.passwordToggleIcon}>{visible ? '🙈' : '👁'}</Text>
|
|
56
|
+
</Pressable>
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const styles = StyleSheet.create({
|
|
62
|
+
field: {
|
|
63
|
+
minHeight: 56,
|
|
64
|
+
borderRadius: m3Shape.cornerExtraSmall,
|
|
65
|
+
backgroundColor: 'transparent',
|
|
66
|
+
borderWidth: 1,
|
|
67
|
+
borderColor: m3Colors.outline,
|
|
68
|
+
paddingHorizontal: spacing.sm,
|
|
69
|
+
justifyContent: 'center',
|
|
70
|
+
},
|
|
71
|
+
fieldInput: { color: m3Colors.onSurface, fontSize: 16, fontFamily: fonts.regular },
|
|
72
|
+
passwordField: { flexDirection: 'row', alignItems: 'center', paddingRight: spacing.sm },
|
|
73
|
+
passwordInput: { flex: 1 },
|
|
74
|
+
passwordToggle: {
|
|
75
|
+
width: 40,
|
|
76
|
+
height: 40,
|
|
77
|
+
alignItems: 'center',
|
|
78
|
+
justifyContent: 'center',
|
|
79
|
+
},
|
|
80
|
+
passwordToggleIcon: { fontSize: 18 },
|
|
81
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Pressable, StyleSheet, Text } from 'react-native';
|
|
2
|
+
import { fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
|
|
3
|
+
|
|
4
|
+
export function ComposeAudiencePill({
|
|
5
|
+
label,
|
|
6
|
+
onPress,
|
|
7
|
+
}: {
|
|
8
|
+
label: string;
|
|
9
|
+
onPress?: () => void;
|
|
10
|
+
}) {
|
|
11
|
+
return (
|
|
12
|
+
<Pressable
|
|
13
|
+
onPress={onPress}
|
|
14
|
+
disabled={!onPress}
|
|
15
|
+
accessibilityRole="button"
|
|
16
|
+
accessibilityLabel={label}
|
|
17
|
+
style={({ pressed }) => [styles.pill, pressed && onPress && { opacity: 0.82 }]}
|
|
18
|
+
>
|
|
19
|
+
<Text style={styles.label}>{label}</Text>
|
|
20
|
+
<Text style={styles.chevron}>{'\u25BE'}</Text>
|
|
21
|
+
</Pressable>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const styles = StyleSheet.create({
|
|
26
|
+
pill: {
|
|
27
|
+
flexDirection: 'row',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
gap: spacing.xs,
|
|
30
|
+
paddingHorizontal: spacing.sm,
|
|
31
|
+
paddingVertical: spacing.xs,
|
|
32
|
+
borderRadius: m3Shape.cornerFull,
|
|
33
|
+
borderWidth: 1,
|
|
34
|
+
borderColor: m3Colors.primary,
|
|
35
|
+
backgroundColor: m3Colors.primaryContainer,
|
|
36
|
+
},
|
|
37
|
+
label: {
|
|
38
|
+
color: m3Colors.onPrimaryContainer,
|
|
39
|
+
fontFamily: fonts.medium,
|
|
40
|
+
fontSize: 13,
|
|
41
|
+
},
|
|
42
|
+
chevron: {
|
|
43
|
+
color: m3Colors.onPrimaryContainer,
|
|
44
|
+
fontSize: 14,
|
|
45
|
+
marginTop: 1,
|
|
46
|
+
},
|
|
47
|
+
});
|