@astacinco/rn-primitives 0.1.0 → 0.2.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 +195 -0
- package/__tests__/Tabs.test.tsx +194 -0
- package/__tests__/Tag.test.tsx +123 -0
- package/__tests__/Timer.test.tsx +208 -0
- package/package.json +10 -6
- package/src/AppFooter/AppFooter.tsx +113 -0
- package/src/AppFooter/index.ts +2 -0
- package/src/AppFooter/types.ts +39 -0
- package/src/AppHeader/AppHeader.tsx +165 -0
- package/src/AppHeader/index.ts +2 -0
- package/src/AppHeader/types.ts +82 -0
- package/src/Avatar/Avatar.tsx +111 -0
- package/src/Avatar/index.ts +2 -0
- package/src/Avatar/types.ts +63 -0
- package/src/Badge/Badge.tsx +150 -0
- package/src/Badge/index.ts +2 -0
- package/src/Badge/types.ts +93 -0
- package/src/Button/Button.tsx +34 -20
- package/src/Button/types.ts +1 -1
- package/src/FloatingTierBadge/FloatingTierBadge.tsx +100 -0
- package/src/FloatingTierBadge/index.ts +2 -0
- package/src/FloatingTierBadge/types.ts +29 -0
- package/src/Input/Input.tsx +8 -23
- package/src/MarkdownViewer/MarkdownViewer.tsx +185 -0
- package/src/MarkdownViewer/index.ts +2 -0
- package/src/MarkdownViewer/types.ts +18 -0
- package/src/Modal/Modal.tsx +136 -0
- package/src/Modal/index.ts +2 -0
- package/src/Modal/types.ts +68 -0
- package/src/ProBadge/ProBadge.tsx +59 -0
- package/src/ProBadge/index.ts +2 -0
- package/src/ProBadge/types.ts +13 -0
- package/src/ProLockOverlay/ProLockOverlay.tsx +106 -0
- package/src/ProLockOverlay/index.ts +2 -0
- package/src/ProLockOverlay/types.ts +22 -0
- package/src/Switch/Switch.tsx +120 -0
- package/src/Switch/index.ts +2 -0
- package/src/Switch/types.ts +58 -0
- package/src/Tabs/Tabs.tsx +137 -0
- package/src/Tabs/index.ts +2 -0
- package/src/Tabs/types.ts +66 -0
- package/src/Tag/Tag.tsx +100 -0
- package/src/Tag/index.ts +2 -0
- package/src/Tag/types.ts +42 -0
- package/src/Timer/Timer.tsx +170 -0
- package/src/Timer/index.ts +2 -0
- package/src/Timer/types.ts +69 -0
- package/src/index.ts +52 -0
package/src/Input/Input.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import { View, TextInput,
|
|
2
|
+
import { View, TextInput, StyleSheet, NativeSyntheticEvent, TextInputFocusEventData } from 'react-native';
|
|
3
3
|
import { useTheme } from '@astacinco/rn-theming';
|
|
4
|
+
import { Text } from '../Text';
|
|
4
5
|
import type { InputProps } from './types';
|
|
5
6
|
|
|
6
7
|
export function Input({
|
|
@@ -36,14 +37,9 @@ export function Input({
|
|
|
36
37
|
<View style={styles.container}>
|
|
37
38
|
{label && (
|
|
38
39
|
<Text
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
color: colors.textSecondary,
|
|
43
|
-
fontSize: typography.fontSize.sm,
|
|
44
|
-
marginBottom: spacing.xs,
|
|
45
|
-
},
|
|
46
|
-
]}
|
|
40
|
+
variant="label"
|
|
41
|
+
color={colors.textSecondary}
|
|
42
|
+
style={{ marginBottom: spacing.xs }}
|
|
47
43
|
>
|
|
48
44
|
{label}
|
|
49
45
|
</Text>
|
|
@@ -70,14 +66,9 @@ export function Input({
|
|
|
70
66
|
/>
|
|
71
67
|
{error && (
|
|
72
68
|
<Text
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
color: colors.error,
|
|
77
|
-
fontSize: typography.fontSize.sm,
|
|
78
|
-
marginTop: spacing.xs,
|
|
79
|
-
},
|
|
80
|
-
]}
|
|
69
|
+
variant="caption"
|
|
70
|
+
color={colors.error}
|
|
71
|
+
style={{ marginTop: spacing.xs }}
|
|
81
72
|
>
|
|
82
73
|
{error}
|
|
83
74
|
</Text>
|
|
@@ -90,14 +81,8 @@ const styles = StyleSheet.create({
|
|
|
90
81
|
container: {
|
|
91
82
|
width: '100%',
|
|
92
83
|
},
|
|
93
|
-
label: {
|
|
94
|
-
fontWeight: '500',
|
|
95
|
-
},
|
|
96
84
|
input: {
|
|
97
85
|
borderWidth: 1,
|
|
98
86
|
borderRadius: 8,
|
|
99
87
|
},
|
|
100
|
-
error: {
|
|
101
|
-
// Error text styles
|
|
102
|
-
},
|
|
103
88
|
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import { useTheme } from '@astacinco/rn-theming';
|
|
4
|
+
import type { MarkdownViewerProps } from './types';
|
|
5
|
+
|
|
6
|
+
// Conditionally import markdown display
|
|
7
|
+
let Markdown: React.ComponentType<any> | null = null;
|
|
8
|
+
try {
|
|
9
|
+
Markdown = require('react-native-markdown-display').default;
|
|
10
|
+
} catch {
|
|
11
|
+
// react-native-markdown-display not installed
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function MarkdownViewer({
|
|
15
|
+
content,
|
|
16
|
+
style,
|
|
17
|
+
testID,
|
|
18
|
+
}: MarkdownViewerProps) {
|
|
19
|
+
const { colors, typography } = useTheme();
|
|
20
|
+
|
|
21
|
+
// Theme-aware markdown styles
|
|
22
|
+
const markdownStyles = {
|
|
23
|
+
body: {
|
|
24
|
+
color: colors.text,
|
|
25
|
+
fontSize: typography.fontSize.md,
|
|
26
|
+
lineHeight: typography.fontSize.md * 1.6,
|
|
27
|
+
},
|
|
28
|
+
heading1: {
|
|
29
|
+
color: colors.text,
|
|
30
|
+
fontSize: typography.fontSize.xxl,
|
|
31
|
+
fontWeight: 'bold' as const,
|
|
32
|
+
marginTop: 16,
|
|
33
|
+
marginBottom: 8,
|
|
34
|
+
},
|
|
35
|
+
heading2: {
|
|
36
|
+
color: colors.text,
|
|
37
|
+
fontSize: typography.fontSize.xl,
|
|
38
|
+
fontWeight: 'bold' as const,
|
|
39
|
+
marginTop: 14,
|
|
40
|
+
marginBottom: 6,
|
|
41
|
+
},
|
|
42
|
+
heading3: {
|
|
43
|
+
color: colors.text,
|
|
44
|
+
fontSize: typography.fontSize.lg,
|
|
45
|
+
fontWeight: '600' as const,
|
|
46
|
+
marginTop: 12,
|
|
47
|
+
marginBottom: 4,
|
|
48
|
+
},
|
|
49
|
+
heading4: {
|
|
50
|
+
color: colors.text,
|
|
51
|
+
fontSize: typography.fontSize.md,
|
|
52
|
+
fontWeight: '600' as const,
|
|
53
|
+
marginTop: 10,
|
|
54
|
+
marginBottom: 4,
|
|
55
|
+
},
|
|
56
|
+
paragraph: {
|
|
57
|
+
marginTop: 0,
|
|
58
|
+
marginBottom: 12,
|
|
59
|
+
},
|
|
60
|
+
link: {
|
|
61
|
+
color: colors.primary,
|
|
62
|
+
},
|
|
63
|
+
blockquote: {
|
|
64
|
+
backgroundColor: colors.surface,
|
|
65
|
+
borderLeftColor: colors.primary,
|
|
66
|
+
borderLeftWidth: 4,
|
|
67
|
+
paddingLeft: 12,
|
|
68
|
+
paddingVertical: 8,
|
|
69
|
+
marginVertical: 8,
|
|
70
|
+
},
|
|
71
|
+
code_inline: {
|
|
72
|
+
backgroundColor: colors.surface,
|
|
73
|
+
color: colors.secondary,
|
|
74
|
+
paddingHorizontal: 6,
|
|
75
|
+
paddingVertical: 2,
|
|
76
|
+
borderRadius: 4,
|
|
77
|
+
fontSize: typography.fontSize.sm,
|
|
78
|
+
fontFamily: 'monospace',
|
|
79
|
+
},
|
|
80
|
+
code_block: {
|
|
81
|
+
backgroundColor: colors.surface,
|
|
82
|
+
padding: 12,
|
|
83
|
+
borderRadius: 8,
|
|
84
|
+
marginVertical: 8,
|
|
85
|
+
},
|
|
86
|
+
fence: {
|
|
87
|
+
backgroundColor: colors.surface,
|
|
88
|
+
padding: 12,
|
|
89
|
+
borderRadius: 8,
|
|
90
|
+
marginVertical: 8,
|
|
91
|
+
fontFamily: 'monospace',
|
|
92
|
+
fontSize: typography.fontSize.sm,
|
|
93
|
+
color: colors.text,
|
|
94
|
+
},
|
|
95
|
+
list_item: {
|
|
96
|
+
marginVertical: 4,
|
|
97
|
+
},
|
|
98
|
+
bullet_list: {
|
|
99
|
+
marginVertical: 8,
|
|
100
|
+
},
|
|
101
|
+
ordered_list: {
|
|
102
|
+
marginVertical: 8,
|
|
103
|
+
},
|
|
104
|
+
table: {
|
|
105
|
+
borderWidth: 1,
|
|
106
|
+
borderColor: colors.border,
|
|
107
|
+
borderRadius: 8,
|
|
108
|
+
marginVertical: 8,
|
|
109
|
+
},
|
|
110
|
+
thead: {
|
|
111
|
+
backgroundColor: colors.surface,
|
|
112
|
+
},
|
|
113
|
+
th: {
|
|
114
|
+
padding: 8,
|
|
115
|
+
borderBottomWidth: 1,
|
|
116
|
+
borderColor: colors.border,
|
|
117
|
+
fontWeight: '600' as const,
|
|
118
|
+
},
|
|
119
|
+
td: {
|
|
120
|
+
padding: 8,
|
|
121
|
+
borderBottomWidth: 1,
|
|
122
|
+
borderColor: colors.border,
|
|
123
|
+
},
|
|
124
|
+
hr: {
|
|
125
|
+
backgroundColor: colors.border,
|
|
126
|
+
height: 1,
|
|
127
|
+
marginVertical: 16,
|
|
128
|
+
},
|
|
129
|
+
strong: {
|
|
130
|
+
fontWeight: 'bold' as const,
|
|
131
|
+
},
|
|
132
|
+
em: {
|
|
133
|
+
fontStyle: 'italic' as const,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Fallback if markdown library not installed
|
|
138
|
+
if (!Markdown) {
|
|
139
|
+
return (
|
|
140
|
+
<View testID={testID} style={[styles.container, style]}>
|
|
141
|
+
<View style={[styles.fallback, { backgroundColor: colors.surface }]}>
|
|
142
|
+
{/* Simple text display without markdown parsing */}
|
|
143
|
+
<View>
|
|
144
|
+
{content.split('\n').map((line, i) => (
|
|
145
|
+
<View key={i} style={{ marginVertical: 2 }}>
|
|
146
|
+
<View>
|
|
147
|
+
{/* Using native Text here as fallback */}
|
|
148
|
+
{React.createElement(
|
|
149
|
+
require('react-native').Text,
|
|
150
|
+
{
|
|
151
|
+
style: {
|
|
152
|
+
color: colors.text,
|
|
153
|
+
fontSize: typography.fontSize.sm,
|
|
154
|
+
fontFamily: 'monospace',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
line
|
|
158
|
+
)}
|
|
159
|
+
</View>
|
|
160
|
+
</View>
|
|
161
|
+
))}
|
|
162
|
+
</View>
|
|
163
|
+
</View>
|
|
164
|
+
</View>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<View testID={testID} style={[styles.container, style]}>
|
|
170
|
+
<Markdown style={markdownStyles}>
|
|
171
|
+
{content}
|
|
172
|
+
</Markdown>
|
|
173
|
+
</View>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const styles = StyleSheet.create({
|
|
178
|
+
container: {
|
|
179
|
+
flex: 1,
|
|
180
|
+
},
|
|
181
|
+
fallback: {
|
|
182
|
+
padding: 12,
|
|
183
|
+
borderRadius: 8,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ViewStyle, StyleProp } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface MarkdownViewerProps {
|
|
4
|
+
/**
|
|
5
|
+
* Markdown content to render
|
|
6
|
+
*/
|
|
7
|
+
content: string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Custom container style
|
|
11
|
+
*/
|
|
12
|
+
style?: StyleProp<ViewStyle>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Test ID for testing
|
|
16
|
+
*/
|
|
17
|
+
testID?: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal Component
|
|
3
|
+
*
|
|
4
|
+
* Theme-aware modal dialog for confirmations and alerts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import {
|
|
9
|
+
Modal as RNModal,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
Pressable,
|
|
12
|
+
Dimensions,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import { useTheme } from '@astacinco/rn-theming';
|
|
15
|
+
import { Text } from '../Text';
|
|
16
|
+
import { Button } from '../Button';
|
|
17
|
+
import { VStack, HStack } from '../Stack';
|
|
18
|
+
import type { ModalProps } from './types';
|
|
19
|
+
|
|
20
|
+
export function Modal({
|
|
21
|
+
visible,
|
|
22
|
+
onDismiss,
|
|
23
|
+
title,
|
|
24
|
+
message,
|
|
25
|
+
actions,
|
|
26
|
+
showCloseButton = true,
|
|
27
|
+
closeOnBackdropPress = true,
|
|
28
|
+
children,
|
|
29
|
+
testID,
|
|
30
|
+
}: ModalProps) {
|
|
31
|
+
const { colors } = useTheme();
|
|
32
|
+
|
|
33
|
+
const handleBackdropPress = () => {
|
|
34
|
+
if (closeOnBackdropPress) {
|
|
35
|
+
onDismiss();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<RNModal
|
|
41
|
+
visible={visible}
|
|
42
|
+
transparent
|
|
43
|
+
animationType="fade"
|
|
44
|
+
onRequestClose={onDismiss}
|
|
45
|
+
testID={testID}
|
|
46
|
+
>
|
|
47
|
+
<Pressable
|
|
48
|
+
style={styles.backdrop}
|
|
49
|
+
onPress={handleBackdropPress}
|
|
50
|
+
>
|
|
51
|
+
<Pressable
|
|
52
|
+
style={[
|
|
53
|
+
styles.container,
|
|
54
|
+
{
|
|
55
|
+
backgroundColor: colors.surface,
|
|
56
|
+
borderColor: colors.border,
|
|
57
|
+
},
|
|
58
|
+
]}
|
|
59
|
+
onPress={(e) => e.stopPropagation()}
|
|
60
|
+
>
|
|
61
|
+
<VStack spacing="md">
|
|
62
|
+
{/* Header */}
|
|
63
|
+
<HStack justify="space-between" align="center">
|
|
64
|
+
<Text variant="subtitle">{title}</Text>
|
|
65
|
+
{showCloseButton && (
|
|
66
|
+
<Pressable
|
|
67
|
+
onPress={onDismiss}
|
|
68
|
+
style={[styles.closeButton, { backgroundColor: colors.surfaceElevated }]}
|
|
69
|
+
hitSlop={8}
|
|
70
|
+
>
|
|
71
|
+
<Text variant="body" color={colors.textSecondary}>✕</Text>
|
|
72
|
+
</Pressable>
|
|
73
|
+
)}
|
|
74
|
+
</HStack>
|
|
75
|
+
|
|
76
|
+
{/* Content */}
|
|
77
|
+
{message && (
|
|
78
|
+
<Text variant="body" color={colors.textSecondary}>
|
|
79
|
+
{message}
|
|
80
|
+
</Text>
|
|
81
|
+
)}
|
|
82
|
+
{children}
|
|
83
|
+
|
|
84
|
+
{/* Actions */}
|
|
85
|
+
{actions && actions.length > 0 && (
|
|
86
|
+
<HStack spacing="sm" justify="end" style={styles.actions}>
|
|
87
|
+
{actions.map((action, index) => (
|
|
88
|
+
<Button
|
|
89
|
+
key={index}
|
|
90
|
+
label={action.label}
|
|
91
|
+
variant={action.variant || (index === actions.length - 1 ? 'primary' : 'outline')}
|
|
92
|
+
onPress={action.onPress}
|
|
93
|
+
size="sm"
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
</HStack>
|
|
97
|
+
)}
|
|
98
|
+
</VStack>
|
|
99
|
+
</Pressable>
|
|
100
|
+
</Pressable>
|
|
101
|
+
</RNModal>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { width } = Dimensions.get('window');
|
|
106
|
+
|
|
107
|
+
const styles = StyleSheet.create({
|
|
108
|
+
backdrop: {
|
|
109
|
+
flex: 1,
|
|
110
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
111
|
+
justifyContent: 'center',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
padding: 24,
|
|
114
|
+
},
|
|
115
|
+
container: {
|
|
116
|
+
width: Math.min(width - 48, 400),
|
|
117
|
+
borderRadius: 16,
|
|
118
|
+
padding: 20,
|
|
119
|
+
borderWidth: 1,
|
|
120
|
+
shadowColor: '#000',
|
|
121
|
+
shadowOffset: { width: 0, height: 8 },
|
|
122
|
+
shadowOpacity: 0.25,
|
|
123
|
+
shadowRadius: 24,
|
|
124
|
+
elevation: 8,
|
|
125
|
+
},
|
|
126
|
+
closeButton: {
|
|
127
|
+
width: 28,
|
|
128
|
+
height: 28,
|
|
129
|
+
borderRadius: 14,
|
|
130
|
+
justifyContent: 'center',
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
},
|
|
133
|
+
actions: {
|
|
134
|
+
marginTop: 8,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ModalAction {
|
|
4
|
+
/**
|
|
5
|
+
* Button label
|
|
6
|
+
*/
|
|
7
|
+
label: string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Press handler
|
|
11
|
+
*/
|
|
12
|
+
onPress: () => void;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Button variant
|
|
16
|
+
* @default 'primary' for last action, 'outline' for others
|
|
17
|
+
*/
|
|
18
|
+
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ModalProps {
|
|
22
|
+
/**
|
|
23
|
+
* Whether the modal is visible
|
|
24
|
+
*/
|
|
25
|
+
visible: boolean;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Callback when modal is dismissed
|
|
29
|
+
*/
|
|
30
|
+
onDismiss: () => void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Modal title
|
|
34
|
+
*/
|
|
35
|
+
title: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Modal message/content
|
|
39
|
+
*/
|
|
40
|
+
message?: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Action buttons
|
|
44
|
+
*/
|
|
45
|
+
actions?: ModalAction[];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether to show close button
|
|
49
|
+
* @default true
|
|
50
|
+
*/
|
|
51
|
+
showCloseButton?: boolean;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether to close on backdrop press
|
|
55
|
+
* @default true
|
|
56
|
+
*/
|
|
57
|
+
closeOnBackdropPress?: boolean;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Custom content to render instead of message
|
|
61
|
+
*/
|
|
62
|
+
children?: ReactNode;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Test ID
|
|
66
|
+
*/
|
|
67
|
+
testID?: string;
|
|
68
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProBadge component
|
|
3
|
+
*
|
|
4
|
+
* Gold badge indicating pro-tier content.
|
|
5
|
+
* Uses themed pro color for consistency across themes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { StyleSheet, View } from 'react-native';
|
|
10
|
+
import { useTheme } from '@astacinco/rn-theming';
|
|
11
|
+
import { Text } from '../Text';
|
|
12
|
+
import type { ProBadgeProps, ProBadgeSize } from './types';
|
|
13
|
+
|
|
14
|
+
const sizeConfig: Record<ProBadgeSize, { paddingH: number; paddingV: number; fontSize: number; radius: number }> = {
|
|
15
|
+
sm: { paddingH: 4, paddingV: 1, fontSize: 9, radius: 3 },
|
|
16
|
+
md: { paddingH: 6, paddingV: 2, fontSize: 11, radius: 4 },
|
|
17
|
+
lg: { paddingH: 8, paddingV: 3, fontSize: 13, radius: 5 },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function ProBadge({ size = 'md' }: ProBadgeProps) {
|
|
21
|
+
const { colors } = useTheme();
|
|
22
|
+
const config = sizeConfig[size];
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<View
|
|
26
|
+
style={[
|
|
27
|
+
styles.badge,
|
|
28
|
+
{
|
|
29
|
+
backgroundColor: colors.pro,
|
|
30
|
+
paddingHorizontal: config.paddingH,
|
|
31
|
+
paddingVertical: config.paddingV,
|
|
32
|
+
borderRadius: config.radius,
|
|
33
|
+
},
|
|
34
|
+
]}
|
|
35
|
+
>
|
|
36
|
+
<Text
|
|
37
|
+
variant="label"
|
|
38
|
+
style={[
|
|
39
|
+
styles.text,
|
|
40
|
+
{ fontSize: config.fontSize },
|
|
41
|
+
]}
|
|
42
|
+
>
|
|
43
|
+
PRO
|
|
44
|
+
</Text>
|
|
45
|
+
</View>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const styles = StyleSheet.create({
|
|
50
|
+
badge: {
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
justifyContent: 'center',
|
|
53
|
+
},
|
|
54
|
+
text: {
|
|
55
|
+
color: '#000',
|
|
56
|
+
fontWeight: '700',
|
|
57
|
+
letterSpacing: 0.5,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProLockOverlay component
|
|
3
|
+
*
|
|
4
|
+
* Displays over locked pro content with blur effect and unlock CTA.
|
|
5
|
+
* Uses themed colors for consistency.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { StyleSheet, View } from 'react-native';
|
|
10
|
+
import { useTheme } from '@astacinco/rn-theming';
|
|
11
|
+
import { Text } from '../Text';
|
|
12
|
+
import { VStack } from '../Stack';
|
|
13
|
+
import { Button } from '../Button';
|
|
14
|
+
import { ProBadge } from '../ProBadge';
|
|
15
|
+
import type { ProLockOverlayProps } from './types';
|
|
16
|
+
|
|
17
|
+
export function ProLockOverlay({
|
|
18
|
+
onUnlockPress,
|
|
19
|
+
message = 'Unlock with Pro to access this content',
|
|
20
|
+
buttonLabel = 'Unlock Pro Content',
|
|
21
|
+
}: ProLockOverlayProps) {
|
|
22
|
+
const { colors } = useTheme();
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<View style={styles.container}>
|
|
26
|
+
{/* Blur/dim overlay */}
|
|
27
|
+
<View
|
|
28
|
+
style={[
|
|
29
|
+
styles.backdrop,
|
|
30
|
+
{ backgroundColor: colors.background },
|
|
31
|
+
]}
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
{/* Content */}
|
|
35
|
+
<View style={styles.content}>
|
|
36
|
+
<VStack spacing="md" align="center">
|
|
37
|
+
{/* Lock icon */}
|
|
38
|
+
<View
|
|
39
|
+
style={[
|
|
40
|
+
styles.lockIcon,
|
|
41
|
+
{
|
|
42
|
+
borderColor: colors.pro,
|
|
43
|
+
backgroundColor: colors.proGlow,
|
|
44
|
+
},
|
|
45
|
+
]}
|
|
46
|
+
>
|
|
47
|
+
<Text style={styles.lockEmoji}>🔒</Text>
|
|
48
|
+
</View>
|
|
49
|
+
|
|
50
|
+
{/* Pro badge */}
|
|
51
|
+
<ProBadge size="lg" />
|
|
52
|
+
|
|
53
|
+
{/* Message */}
|
|
54
|
+
<Text
|
|
55
|
+
variant="body"
|
|
56
|
+
color={colors.textSecondary}
|
|
57
|
+
style={styles.message}
|
|
58
|
+
>
|
|
59
|
+
{message}
|
|
60
|
+
</Text>
|
|
61
|
+
|
|
62
|
+
{/* Unlock button */}
|
|
63
|
+
<Button
|
|
64
|
+
label={buttonLabel}
|
|
65
|
+
onPress={onUnlockPress}
|
|
66
|
+
variant="primary"
|
|
67
|
+
size="md"
|
|
68
|
+
/>
|
|
69
|
+
</VStack>
|
|
70
|
+
</View>
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const styles = StyleSheet.create({
|
|
76
|
+
container: {
|
|
77
|
+
...StyleSheet.absoluteFillObject,
|
|
78
|
+
justifyContent: 'center',
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
borderRadius: 12,
|
|
81
|
+
overflow: 'hidden',
|
|
82
|
+
},
|
|
83
|
+
backdrop: {
|
|
84
|
+
...StyleSheet.absoluteFillObject,
|
|
85
|
+
opacity: 0.92,
|
|
86
|
+
},
|
|
87
|
+
content: {
|
|
88
|
+
padding: 24,
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
},
|
|
91
|
+
lockIcon: {
|
|
92
|
+
width: 56,
|
|
93
|
+
height: 56,
|
|
94
|
+
borderRadius: 28,
|
|
95
|
+
borderWidth: 2,
|
|
96
|
+
justifyContent: 'center',
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
},
|
|
99
|
+
lockEmoji: {
|
|
100
|
+
fontSize: 24,
|
|
101
|
+
},
|
|
102
|
+
message: {
|
|
103
|
+
textAlign: 'center',
|
|
104
|
+
maxWidth: 280,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProLockOverlay types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ProLockOverlayProps {
|
|
6
|
+
/**
|
|
7
|
+
* Callback when unlock button is pressed
|
|
8
|
+
*/
|
|
9
|
+
onUnlockPress: () => void;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Custom message to display
|
|
13
|
+
* @default 'Unlock with Pro to access this content'
|
|
14
|
+
*/
|
|
15
|
+
message?: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Button label text
|
|
19
|
+
* @default 'Unlock Pro Content'
|
|
20
|
+
*/
|
|
21
|
+
buttonLabel?: string;
|
|
22
|
+
}
|