@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.
Files changed (48) hide show
  1. package/README.md +195 -0
  2. package/__tests__/Tabs.test.tsx +194 -0
  3. package/__tests__/Tag.test.tsx +123 -0
  4. package/__tests__/Timer.test.tsx +208 -0
  5. package/package.json +10 -6
  6. package/src/AppFooter/AppFooter.tsx +113 -0
  7. package/src/AppFooter/index.ts +2 -0
  8. package/src/AppFooter/types.ts +39 -0
  9. package/src/AppHeader/AppHeader.tsx +165 -0
  10. package/src/AppHeader/index.ts +2 -0
  11. package/src/AppHeader/types.ts +82 -0
  12. package/src/Avatar/Avatar.tsx +111 -0
  13. package/src/Avatar/index.ts +2 -0
  14. package/src/Avatar/types.ts +63 -0
  15. package/src/Badge/Badge.tsx +150 -0
  16. package/src/Badge/index.ts +2 -0
  17. package/src/Badge/types.ts +93 -0
  18. package/src/Button/Button.tsx +34 -20
  19. package/src/Button/types.ts +1 -1
  20. package/src/FloatingTierBadge/FloatingTierBadge.tsx +100 -0
  21. package/src/FloatingTierBadge/index.ts +2 -0
  22. package/src/FloatingTierBadge/types.ts +29 -0
  23. package/src/Input/Input.tsx +8 -23
  24. package/src/MarkdownViewer/MarkdownViewer.tsx +185 -0
  25. package/src/MarkdownViewer/index.ts +2 -0
  26. package/src/MarkdownViewer/types.ts +18 -0
  27. package/src/Modal/Modal.tsx +136 -0
  28. package/src/Modal/index.ts +2 -0
  29. package/src/Modal/types.ts +68 -0
  30. package/src/ProBadge/ProBadge.tsx +59 -0
  31. package/src/ProBadge/index.ts +2 -0
  32. package/src/ProBadge/types.ts +13 -0
  33. package/src/ProLockOverlay/ProLockOverlay.tsx +106 -0
  34. package/src/ProLockOverlay/index.ts +2 -0
  35. package/src/ProLockOverlay/types.ts +22 -0
  36. package/src/Switch/Switch.tsx +120 -0
  37. package/src/Switch/index.ts +2 -0
  38. package/src/Switch/types.ts +58 -0
  39. package/src/Tabs/Tabs.tsx +137 -0
  40. package/src/Tabs/index.ts +2 -0
  41. package/src/Tabs/types.ts +66 -0
  42. package/src/Tag/Tag.tsx +100 -0
  43. package/src/Tag/index.ts +2 -0
  44. package/src/Tag/types.ts +42 -0
  45. package/src/Timer/Timer.tsx +170 -0
  46. package/src/Timer/index.ts +2 -0
  47. package/src/Timer/types.ts +69 -0
  48. package/src/index.ts +52 -0
@@ -1,6 +1,7 @@
1
1
  import React, { useState } from 'react';
2
- import { View, TextInput, Text, StyleSheet, NativeSyntheticEvent, TextInputFocusEventData } from 'react-native';
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
- style={[
40
- styles.label,
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
- style={[
74
- styles.error,
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,2 @@
1
+ export { MarkdownViewer } from './MarkdownViewer';
2
+ export type { MarkdownViewerProps } from './types';
@@ -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,2 @@
1
+ export { Modal } from './Modal';
2
+ export type { ModalProps, ModalAction } from './types';
@@ -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,2 @@
1
+ export { ProBadge } from './ProBadge';
2
+ export type { ProBadgeProps, ProBadgeSize } from './types';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * ProBadge types
3
+ */
4
+
5
+ export type ProBadgeSize = 'sm' | 'md' | 'lg';
6
+
7
+ export interface ProBadgeProps {
8
+ /**
9
+ * Size of the badge
10
+ * @default 'md'
11
+ */
12
+ size?: ProBadgeSize;
13
+ }
@@ -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,2 @@
1
+ export { ProLockOverlay } from './ProLockOverlay';
2
+ export type { ProLockOverlayProps } from './types';
@@ -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
+ }