syzygy-ui-rn 2.0.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/LICENSE +21 -0
- package/README.md +151 -0
- package/package.json +56 -0
- package/src/__tests__/components.test.tsx +368 -0
- package/src/__tests__/tokens.test.ts +47 -0
- package/src/components/badges/Badge.tsx +74 -0
- package/src/components/buttons/DestructiveButton.tsx +82 -0
- package/src/components/buttons/GhostButton.tsx +77 -0
- package/src/components/buttons/IconButton.tsx +61 -0
- package/src/components/buttons/PrimaryButton.tsx +82 -0
- package/src/components/buttons/SecondaryButton.tsx +84 -0
- package/src/components/cards/CardView.tsx +45 -0
- package/src/components/display/Avatar.tsx +53 -0
- package/src/components/display/Chip.tsx +64 -0
- package/src/components/display/CountBadge.tsx +58 -0
- package/src/components/display/DividerLine.tsx +29 -0
- package/src/components/display/LazyImageView.tsx +57 -0
- package/src/components/display/ListRow.tsx +76 -0
- package/src/components/display/SectionHeader.tsx +55 -0
- package/src/components/display/StarRatingView.tsx +70 -0
- package/src/components/feedback/EmptyStateView.tsx +93 -0
- package/src/components/feedback/ErrorStateView.tsx +75 -0
- package/src/components/feedback/LoadingView.tsx +57 -0
- package/src/components/feedback/ProgressBar.tsx +42 -0
- package/src/components/feedback/PullToRefresh.tsx +38 -0
- package/src/components/feedback/ShimmerView.tsx +47 -0
- package/src/components/feedback/ToastView.tsx +71 -0
- package/src/components/inputs/CheckboxInput.tsx +73 -0
- package/src/components/inputs/Dropdown.tsx +110 -0
- package/src/components/inputs/QuantityStepper.tsx +81 -0
- package/src/components/inputs/RadioButtonInput.tsx +73 -0
- package/src/components/inputs/SearchInput.tsx +107 -0
- package/src/components/inputs/SecureInput.tsx +105 -0
- package/src/components/inputs/SegmentedControl.tsx +70 -0
- package/src/components/inputs/SliderInput.tsx +131 -0
- package/src/components/inputs/TextInput.tsx +90 -0
- package/src/components/inputs/ToggleSwitch.tsx +50 -0
- package/src/components/layout/KeyboardAvoidingScrollView.tsx +40 -0
- package/src/components/navigation/AppBar.tsx +53 -0
- package/src/components/navigation/BackButton.tsx +63 -0
- package/src/components/navigation/BottomNavigationBar.tsx +65 -0
- package/src/components/navigation/PagerView.tsx +40 -0
- package/src/components/navigation/TabBar.tsx +68 -0
- package/src/components/navigation/TabBarItem.ts +8 -0
- package/src/components/overlay/BottomSheet.tsx +64 -0
- package/src/components/overlay/CollapsibleView.tsx +82 -0
- package/src/components/overlay/ModalDialog.tsx +47 -0
- package/src/index.ts +54 -0
- package/src/tokens/colors.ts +84 -0
- package/src/tokens/index.ts +4 -0
- package/src/tokens/radius.ts +8 -0
- package/src/tokens/spacing.ts +10 -0
- package/src/tokens/typography.ts +27 -0
- package/src/transitions/navigationTransitions.ts +66 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleProp, StyleSheet, View, ViewStyle, useColorScheme } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { getColors } from '../../tokens/colors';
|
|
5
|
+
|
|
6
|
+
export interface DividerLineProps {
|
|
7
|
+
style?: StyleProp<ViewStyle>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A hairline horizontal rule. */
|
|
11
|
+
export const DividerLine: React.FC<DividerLineProps> = ({ style }) => {
|
|
12
|
+
const scheme = useColorScheme();
|
|
13
|
+
const colors = getColors(scheme);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<View
|
|
17
|
+
accessibilityElementsHidden
|
|
18
|
+
importantForAccessibility="no"
|
|
19
|
+
style={[styles.line, { backgroundColor: colors.border }, style]}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const styles = StyleSheet.create({
|
|
25
|
+
line: {
|
|
26
|
+
height: StyleSheet.hairlineWidth,
|
|
27
|
+
width: '100%',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Image, StyleProp, StyleSheet, Text, View, ViewStyle, useColorScheme } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { getColors } from '../../tokens/colors';
|
|
5
|
+
import { ShimmerView } from '../feedback/ShimmerView';
|
|
6
|
+
|
|
7
|
+
export interface LazyImageViewProps {
|
|
8
|
+
uri: string | null | undefined;
|
|
9
|
+
style?: StyleProp<ViewStyle>;
|
|
10
|
+
resizeMode?: 'cover' | 'contain' | 'stretch' | 'center';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Asynchronously loads a remote image using React Native's core `Image`
|
|
15
|
+
* component (which handles async loading and caching natively — no
|
|
16
|
+
* third-party image-loading dependency needed), showing a `ShimmerView`
|
|
17
|
+
* placeholder while loading and a fallback glyph if the load fails.
|
|
18
|
+
*/
|
|
19
|
+
export const LazyImageView: React.FC<LazyImageViewProps> = ({ uri, style, resizeMode = 'cover' }) => {
|
|
20
|
+
const scheme = useColorScheme();
|
|
21
|
+
const colors = getColors(scheme);
|
|
22
|
+
const [status, setStatus] = useState<'loading' | 'loaded' | 'error'>(uri ? 'loading' : 'error');
|
|
23
|
+
|
|
24
|
+
if (!uri || status === 'error') {
|
|
25
|
+
return (
|
|
26
|
+
<View
|
|
27
|
+
style={[styles.fallback, { backgroundColor: colors.surfaceAlt }, style]}
|
|
28
|
+
accessibilityLabel="Image failed to load"
|
|
29
|
+
>
|
|
30
|
+
<Text style={{ color: colors.textSecondary }}>{'🖼'}</Text>
|
|
31
|
+
</View>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<View style={style}>
|
|
37
|
+
{status === 'loading' ? (
|
|
38
|
+
<ShimmerView style={StyleSheet.absoluteFill} />
|
|
39
|
+
) : null}
|
|
40
|
+
<Image
|
|
41
|
+
source={{ uri }}
|
|
42
|
+
resizeMode={resizeMode}
|
|
43
|
+
onLoad={() => setStatus('loaded')}
|
|
44
|
+
onError={() => setStatus('error')}
|
|
45
|
+
accessibilityLabel="Image"
|
|
46
|
+
style={StyleSheet.absoluteFillObject}
|
|
47
|
+
/>
|
|
48
|
+
</View>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const styles = StyleSheet.create({
|
|
53
|
+
fallback: {
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
justifyContent: 'center',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
StyleProp,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Text,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
useColorScheme,
|
|
8
|
+
View,
|
|
9
|
+
ViewStyle,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
|
|
12
|
+
import { getColors } from '../../tokens/colors';
|
|
13
|
+
import { spacing } from '../../tokens/spacing';
|
|
14
|
+
import { fontSizes } from '../../tokens/typography';
|
|
15
|
+
|
|
16
|
+
export interface ListRowProps {
|
|
17
|
+
title: string;
|
|
18
|
+
subtitle?: string;
|
|
19
|
+
leading?: React.ReactNode;
|
|
20
|
+
trailing?: React.ReactNode;
|
|
21
|
+
onPress?: () => void;
|
|
22
|
+
style?: StyleProp<ViewStyle>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A styled, optionally-tappable row wrapper with a leading slot, title,
|
|
26
|
+
* subtitle, and a trailing accessory slot. */
|
|
27
|
+
export const ListRow: React.FC<ListRowProps> = ({
|
|
28
|
+
title,
|
|
29
|
+
subtitle,
|
|
30
|
+
leading,
|
|
31
|
+
trailing,
|
|
32
|
+
onPress,
|
|
33
|
+
style,
|
|
34
|
+
}) => {
|
|
35
|
+
const scheme = useColorScheme();
|
|
36
|
+
const colors = getColors(scheme);
|
|
37
|
+
|
|
38
|
+
const content = (
|
|
39
|
+
<View
|
|
40
|
+
style={[styles.container, style]}
|
|
41
|
+
accessibilityLabel={subtitle ? `${title}, ${subtitle}` : title}
|
|
42
|
+
>
|
|
43
|
+
{leading}
|
|
44
|
+
<View style={styles.textContainer}>
|
|
45
|
+
<Text style={{ color: colors.textPrimary, fontSize: fontSizes.md }}>{title}</Text>
|
|
46
|
+
{subtitle ? (
|
|
47
|
+
<Text style={{ color: colors.textSecondary, fontSize: fontSizes.sm }}>{subtitle}</Text>
|
|
48
|
+
) : null}
|
|
49
|
+
</View>
|
|
50
|
+
{trailing}
|
|
51
|
+
</View>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (onPress) {
|
|
55
|
+
return (
|
|
56
|
+
<TouchableOpacity onPress={onPress} accessibilityRole="button">
|
|
57
|
+
{content}
|
|
58
|
+
</TouchableOpacity>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return content;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const styles = StyleSheet.create({
|
|
65
|
+
container: {
|
|
66
|
+
minHeight: 44,
|
|
67
|
+
flexDirection: 'row',
|
|
68
|
+
alignItems: 'center',
|
|
69
|
+
paddingHorizontal: spacing.md,
|
|
70
|
+
paddingVertical: spacing.sm,
|
|
71
|
+
},
|
|
72
|
+
textContainer: {
|
|
73
|
+
flex: 1,
|
|
74
|
+
marginLeft: spacing.sm,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
StyleProp,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Text,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
useColorScheme,
|
|
8
|
+
View,
|
|
9
|
+
ViewStyle,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
|
|
12
|
+
import { getColors } from '../../tokens/colors';
|
|
13
|
+
import { spacing } from '../../tokens/spacing';
|
|
14
|
+
import { fontSizes, fontWeights } from '../../tokens/typography';
|
|
15
|
+
|
|
16
|
+
export interface SectionHeaderProps {
|
|
17
|
+
title: string;
|
|
18
|
+
actionLabel?: string;
|
|
19
|
+
onActionPress?: () => void;
|
|
20
|
+
style?: StyleProp<ViewStyle>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A section title with an optional trailing text action (e.g. "See All"). */
|
|
24
|
+
export const SectionHeader: React.FC<SectionHeaderProps> = ({
|
|
25
|
+
title,
|
|
26
|
+
actionLabel,
|
|
27
|
+
onActionPress,
|
|
28
|
+
style,
|
|
29
|
+
}) => {
|
|
30
|
+
const scheme = useColorScheme();
|
|
31
|
+
const colors = getColors(scheme);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<View style={[styles.container, style]} accessibilityRole="header">
|
|
35
|
+
<Text style={{ color: colors.textPrimary, fontSize: fontSizes.lg, fontWeight: fontWeights.semibold }}>
|
|
36
|
+
{title}
|
|
37
|
+
</Text>
|
|
38
|
+
{actionLabel && onActionPress ? (
|
|
39
|
+
<TouchableOpacity onPress={onActionPress} accessibilityRole="button" accessibilityLabel={actionLabel}>
|
|
40
|
+
<Text style={{ color: colors.primary, fontSize: fontSizes.sm }}>{actionLabel}</Text>
|
|
41
|
+
</TouchableOpacity>
|
|
42
|
+
) : null}
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const styles = StyleSheet.create({
|
|
48
|
+
container: {
|
|
49
|
+
minHeight: 32,
|
|
50
|
+
flexDirection: 'row',
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
justifyContent: 'space-between',
|
|
53
|
+
paddingHorizontal: spacing.md,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleProp, StyleSheet, Text, TouchableOpacity, View, ViewStyle, useColorScheme } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { getColors } from '../../tokens/colors';
|
|
5
|
+
|
|
6
|
+
export interface StarRatingViewProps {
|
|
7
|
+
rating: number;
|
|
8
|
+
maxRating?: number;
|
|
9
|
+
onRatingChanged?: (rating: number) => void;
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A star rating display. Pass `onRatingChanged` to make it interactively
|
|
15
|
+
* tappable; omit it for a read-only display.
|
|
16
|
+
*/
|
|
17
|
+
export const StarRatingView: React.FC<StarRatingViewProps> = ({
|
|
18
|
+
rating,
|
|
19
|
+
maxRating = 5,
|
|
20
|
+
onRatingChanged,
|
|
21
|
+
style,
|
|
22
|
+
}) => {
|
|
23
|
+
const scheme = useColorScheme();
|
|
24
|
+
const colors = getColors(scheme);
|
|
25
|
+
const stars = Array.from({ length: maxRating }, (_, i) => i + 1);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<View
|
|
29
|
+
style={[styles.container, style]}
|
|
30
|
+
accessibilityLabel={`Rating: ${rating} out of ${maxRating} stars`}
|
|
31
|
+
>
|
|
32
|
+
{stars.map((star) => {
|
|
33
|
+
const filled = star <= rating;
|
|
34
|
+
const glyph = filled ? '★' : '☆';
|
|
35
|
+
const color = filled ? colors.warning : colors.textSecondary;
|
|
36
|
+
|
|
37
|
+
if (!onRatingChanged) {
|
|
38
|
+
return (
|
|
39
|
+
<Text key={star} style={{ color, fontSize: 20 }}>
|
|
40
|
+
{glyph}
|
|
41
|
+
</Text>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return (
|
|
45
|
+
<TouchableOpacity
|
|
46
|
+
key={star}
|
|
47
|
+
onPress={() => onRatingChanged(star)}
|
|
48
|
+
accessibilityRole="button"
|
|
49
|
+
accessibilityLabel={`${star} star${star === 1 ? '' : 's'}`}
|
|
50
|
+
style={styles.star}
|
|
51
|
+
>
|
|
52
|
+
<Text style={{ color, fontSize: 20 }}>{glyph}</Text>
|
|
53
|
+
</TouchableOpacity>
|
|
54
|
+
);
|
|
55
|
+
})}
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const styles = StyleSheet.create({
|
|
61
|
+
container: {
|
|
62
|
+
flexDirection: 'row',
|
|
63
|
+
},
|
|
64
|
+
star: {
|
|
65
|
+
minWidth: 44,
|
|
66
|
+
minHeight: 44,
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
justifyContent: 'center',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
StyleProp,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Text,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
useColorScheme,
|
|
8
|
+
View,
|
|
9
|
+
ViewStyle,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
|
|
12
|
+
import { getColors } from '../../tokens/colors';
|
|
13
|
+
import { radius } from '../../tokens/radius';
|
|
14
|
+
import { spacing } from '../../tokens/spacing';
|
|
15
|
+
import { fontSizes, fontWeights } from '../../tokens/typography';
|
|
16
|
+
|
|
17
|
+
export interface EmptyStateViewProps {
|
|
18
|
+
icon?: React.ReactNode;
|
|
19
|
+
title: string;
|
|
20
|
+
subtitle?: string;
|
|
21
|
+
ctaLabel?: string;
|
|
22
|
+
onCtaPress?: () => void;
|
|
23
|
+
style?: StyleProp<ViewStyle>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const EmptyStateView: React.FC<EmptyStateViewProps> = ({
|
|
27
|
+
icon,
|
|
28
|
+
title,
|
|
29
|
+
subtitle,
|
|
30
|
+
ctaLabel,
|
|
31
|
+
onCtaPress,
|
|
32
|
+
style,
|
|
33
|
+
}) => {
|
|
34
|
+
const scheme = useColorScheme();
|
|
35
|
+
const colors = getColors(scheme);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<View style={[styles.container, style]} accessibilityRole="text">
|
|
39
|
+
{icon ? <View style={styles.icon}>{icon}</View> : null}
|
|
40
|
+
<Text style={[styles.title, { color: colors.textPrimary }]}>{title}</Text>
|
|
41
|
+
{subtitle ? (
|
|
42
|
+
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>
|
|
43
|
+
{subtitle}
|
|
44
|
+
</Text>
|
|
45
|
+
) : null}
|
|
46
|
+
{ctaLabel && onCtaPress ? (
|
|
47
|
+
<TouchableOpacity
|
|
48
|
+
onPress={onCtaPress}
|
|
49
|
+
accessibilityRole="button"
|
|
50
|
+
accessibilityLabel={ctaLabel}
|
|
51
|
+
style={[styles.cta, { backgroundColor: colors.primary }]}
|
|
52
|
+
>
|
|
53
|
+
<Text style={[styles.ctaText, { color: colors.primaryText }]}>
|
|
54
|
+
{ctaLabel}
|
|
55
|
+
</Text>
|
|
56
|
+
</TouchableOpacity>
|
|
57
|
+
) : null}
|
|
58
|
+
</View>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const styles = StyleSheet.create({
|
|
63
|
+
container: {
|
|
64
|
+
alignItems: 'center',
|
|
65
|
+
justifyContent: 'center',
|
|
66
|
+
padding: spacing.xl,
|
|
67
|
+
},
|
|
68
|
+
icon: {
|
|
69
|
+
marginBottom: spacing.md,
|
|
70
|
+
},
|
|
71
|
+
title: {
|
|
72
|
+
fontSize: fontSizes.lg,
|
|
73
|
+
fontWeight: fontWeights.semibold,
|
|
74
|
+
textAlign: 'center',
|
|
75
|
+
},
|
|
76
|
+
subtitle: {
|
|
77
|
+
fontSize: fontSizes.sm,
|
|
78
|
+
textAlign: 'center',
|
|
79
|
+
marginTop: spacing.xs,
|
|
80
|
+
},
|
|
81
|
+
cta: {
|
|
82
|
+
minHeight: 44,
|
|
83
|
+
paddingHorizontal: spacing.lg,
|
|
84
|
+
justifyContent: 'center',
|
|
85
|
+
borderRadius: radius.md,
|
|
86
|
+
marginTop: spacing.md,
|
|
87
|
+
},
|
|
88
|
+
ctaText: {
|
|
89
|
+
fontSize: fontSizes.md,
|
|
90
|
+
fontWeight: fontWeights.semibold,
|
|
91
|
+
textAlign: 'center',
|
|
92
|
+
},
|
|
93
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleProp, StyleSheet, Text, TouchableOpacity, useColorScheme, View, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { getColors } from '../../tokens/colors';
|
|
5
|
+
import { radius } from '../../tokens/radius';
|
|
6
|
+
import { spacing } from '../../tokens/spacing';
|
|
7
|
+
import { fontSizes, fontWeights } from '../../tokens/typography';
|
|
8
|
+
|
|
9
|
+
export interface ErrorStateViewProps {
|
|
10
|
+
title: string;
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
retryLabel?: string;
|
|
13
|
+
onRetryPress: () => void;
|
|
14
|
+
style?: StyleProp<ViewStyle>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An icon, title, subtitle, and retry action for error states. Mirrors
|
|
19
|
+
* `EmptyStateView`'s structure with a destructive-tinted icon and a
|
|
20
|
+
* mandatory retry action.
|
|
21
|
+
*/
|
|
22
|
+
export const ErrorStateView: React.FC<ErrorStateViewProps> = ({
|
|
23
|
+
title,
|
|
24
|
+
subtitle,
|
|
25
|
+
retryLabel = 'Retry',
|
|
26
|
+
onRetryPress,
|
|
27
|
+
style,
|
|
28
|
+
}) => {
|
|
29
|
+
const scheme = useColorScheme();
|
|
30
|
+
const colors = getColors(scheme);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<View style={[styles.container, style]} accessibilityRole="text">
|
|
34
|
+
<Text style={{ fontSize: 40, color: colors.destructive }}>{'⚠'}</Text>
|
|
35
|
+
<Text style={[styles.title, { color: colors.textPrimary }]}>{title}</Text>
|
|
36
|
+
{subtitle ? (
|
|
37
|
+
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>{subtitle}</Text>
|
|
38
|
+
) : null}
|
|
39
|
+
<TouchableOpacity
|
|
40
|
+
onPress={onRetryPress}
|
|
41
|
+
accessibilityRole="button"
|
|
42
|
+
accessibilityLabel={retryLabel}
|
|
43
|
+
style={[styles.cta, { backgroundColor: colors.primary }]}
|
|
44
|
+
>
|
|
45
|
+
<Text style={{ color: colors.primaryText, fontWeight: fontWeights.semibold }}>{retryLabel}</Text>
|
|
46
|
+
</TouchableOpacity>
|
|
47
|
+
</View>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const styles = StyleSheet.create({
|
|
52
|
+
container: {
|
|
53
|
+
alignItems: 'center',
|
|
54
|
+
justifyContent: 'center',
|
|
55
|
+
padding: spacing.xl,
|
|
56
|
+
},
|
|
57
|
+
title: {
|
|
58
|
+
fontSize: fontSizes.lg,
|
|
59
|
+
fontWeight: fontWeights.semibold,
|
|
60
|
+
textAlign: 'center',
|
|
61
|
+
marginTop: spacing.sm,
|
|
62
|
+
},
|
|
63
|
+
subtitle: {
|
|
64
|
+
fontSize: fontSizes.sm,
|
|
65
|
+
textAlign: 'center',
|
|
66
|
+
marginTop: spacing.xs,
|
|
67
|
+
},
|
|
68
|
+
cta: {
|
|
69
|
+
minHeight: 44,
|
|
70
|
+
paddingHorizontal: spacing.lg,
|
|
71
|
+
justifyContent: 'center',
|
|
72
|
+
borderRadius: radius.md,
|
|
73
|
+
marginTop: spacing.md,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
StyleProp,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Text,
|
|
7
|
+
useColorScheme,
|
|
8
|
+
View,
|
|
9
|
+
ViewStyle,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
|
|
12
|
+
import { getColors } from '../../tokens/colors';
|
|
13
|
+
import { spacing } from '../../tokens/spacing';
|
|
14
|
+
import { fontSizes } from '../../tokens/typography';
|
|
15
|
+
|
|
16
|
+
export interface LoadingViewProps {
|
|
17
|
+
message?: string;
|
|
18
|
+
style?: StyleProp<ViewStyle>;
|
|
19
|
+
accessibilityLabel?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const LoadingView: React.FC<LoadingViewProps> = ({
|
|
23
|
+
message,
|
|
24
|
+
style,
|
|
25
|
+
accessibilityLabel,
|
|
26
|
+
}) => {
|
|
27
|
+
const scheme = useColorScheme();
|
|
28
|
+
const colors = getColors(scheme);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View
|
|
32
|
+
style={[styles.container, style]}
|
|
33
|
+
accessibilityRole="progressbar"
|
|
34
|
+
accessibilityLabel={accessibilityLabel ?? message ?? 'Loading'}
|
|
35
|
+
>
|
|
36
|
+
<ActivityIndicator size="large" color={colors.primary} />
|
|
37
|
+
{message ? (
|
|
38
|
+
<Text style={[styles.message, { color: colors.textSecondary }]}>
|
|
39
|
+
{message}
|
|
40
|
+
</Text>
|
|
41
|
+
) : null}
|
|
42
|
+
</View>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const styles = StyleSheet.create({
|
|
47
|
+
container: {
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
justifyContent: 'center',
|
|
50
|
+
padding: spacing.lg,
|
|
51
|
+
},
|
|
52
|
+
message: {
|
|
53
|
+
marginTop: spacing.sm,
|
|
54
|
+
fontSize: fontSizes.sm,
|
|
55
|
+
textAlign: 'center',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleProp, StyleSheet, View, ViewStyle, useColorScheme } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { getColors } from '../../tokens/colors';
|
|
5
|
+
import { radius } from '../../tokens/radius';
|
|
6
|
+
|
|
7
|
+
export interface ProgressBarProps {
|
|
8
|
+
progress: number;
|
|
9
|
+
style?: StyleProp<ViewStyle>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A determinate linear progress indicator. */
|
|
13
|
+
export const ProgressBar: React.FC<ProgressBarProps> = ({ progress, style }) => {
|
|
14
|
+
const scheme = useColorScheme();
|
|
15
|
+
const colors = getColors(scheme);
|
|
16
|
+
const clamped = Math.min(1, Math.max(0, progress));
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View
|
|
20
|
+
style={[styles.track, { backgroundColor: colors.border }, style]}
|
|
21
|
+
accessibilityRole="progressbar"
|
|
22
|
+
accessibilityValue={{ min: 0, max: 1, now: clamped }}
|
|
23
|
+
>
|
|
24
|
+
<View style={[styles.fill, { width: `${clamped * 100}%`, backgroundColor: colors.primary }]} />
|
|
25
|
+
</View>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const HEIGHT = 8;
|
|
30
|
+
|
|
31
|
+
const styles = StyleSheet.create({
|
|
32
|
+
track: {
|
|
33
|
+
height: HEIGHT,
|
|
34
|
+
borderRadius: radius.full,
|
|
35
|
+
overflow: 'hidden',
|
|
36
|
+
width: '100%',
|
|
37
|
+
},
|
|
38
|
+
fill: {
|
|
39
|
+
height: HEIGHT,
|
|
40
|
+
borderRadius: radius.full,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { RefreshControl, ScrollView, StyleProp, ViewStyle, useColorScheme } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { getColors } from '../../tokens/colors';
|
|
5
|
+
|
|
6
|
+
export interface PullToRefreshProps {
|
|
7
|
+
refreshing: boolean;
|
|
8
|
+
onRefresh: () => void;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A scrollable container with native pull-to-refresh wired to a refresh handler. */
|
|
14
|
+
export const PullToRefresh: React.FC<PullToRefreshProps> = ({
|
|
15
|
+
refreshing,
|
|
16
|
+
onRefresh,
|
|
17
|
+
children,
|
|
18
|
+
style,
|
|
19
|
+
}) => {
|
|
20
|
+
const scheme = useColorScheme();
|
|
21
|
+
const colors = getColors(scheme);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<ScrollView
|
|
25
|
+
style={style}
|
|
26
|
+
refreshControl={
|
|
27
|
+
<RefreshControl
|
|
28
|
+
refreshing={refreshing}
|
|
29
|
+
onRefresh={onRefresh}
|
|
30
|
+
tintColor={colors.primary}
|
|
31
|
+
colors={[colors.primary]}
|
|
32
|
+
/>
|
|
33
|
+
}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</ScrollView>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, StyleProp, StyleSheet, ViewStyle, useColorScheme } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { getColors } from '../../tokens/colors';
|
|
5
|
+
import { radius } from '../../tokens/radius';
|
|
6
|
+
|
|
7
|
+
export interface ShimmerViewProps {
|
|
8
|
+
style?: StyleProp<ViewStyle>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** An animated skeleton placeholder for list/table rows while content loads. */
|
|
12
|
+
export const ShimmerView: React.FC<ShimmerViewProps> = ({ style }) => {
|
|
13
|
+
const scheme = useColorScheme();
|
|
14
|
+
const colors = getColors(scheme);
|
|
15
|
+
const opacity = useRef(new Animated.Value(0.3)).current;
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const loop = Animated.loop(
|
|
19
|
+
Animated.sequence([
|
|
20
|
+
Animated.timing(opacity, { toValue: 0.7, duration: 700, useNativeDriver: true }),
|
|
21
|
+
Animated.timing(opacity, { toValue: 0.3, duration: 700, useNativeDriver: true }),
|
|
22
|
+
])
|
|
23
|
+
);
|
|
24
|
+
loop.start();
|
|
25
|
+
return () => loop.stop();
|
|
26
|
+
}, [opacity]);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Animated.View
|
|
30
|
+
accessibilityElementsHidden
|
|
31
|
+
importantForAccessibility="no"
|
|
32
|
+
style={[
|
|
33
|
+
styles.base,
|
|
34
|
+
{ backgroundColor: colors.surfaceAlt, opacity },
|
|
35
|
+
style,
|
|
36
|
+
]}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const styles = StyleSheet.create({
|
|
42
|
+
base: {
|
|
43
|
+
height: 16,
|
|
44
|
+
borderRadius: radius.sm,
|
|
45
|
+
width: '100%',
|
|
46
|
+
},
|
|
47
|
+
});
|