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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/package.json +56 -0
  4. package/src/__tests__/components.test.tsx +368 -0
  5. package/src/__tests__/tokens.test.ts +47 -0
  6. package/src/components/badges/Badge.tsx +74 -0
  7. package/src/components/buttons/DestructiveButton.tsx +82 -0
  8. package/src/components/buttons/GhostButton.tsx +77 -0
  9. package/src/components/buttons/IconButton.tsx +61 -0
  10. package/src/components/buttons/PrimaryButton.tsx +82 -0
  11. package/src/components/buttons/SecondaryButton.tsx +84 -0
  12. package/src/components/cards/CardView.tsx +45 -0
  13. package/src/components/display/Avatar.tsx +53 -0
  14. package/src/components/display/Chip.tsx +64 -0
  15. package/src/components/display/CountBadge.tsx +58 -0
  16. package/src/components/display/DividerLine.tsx +29 -0
  17. package/src/components/display/LazyImageView.tsx +57 -0
  18. package/src/components/display/ListRow.tsx +76 -0
  19. package/src/components/display/SectionHeader.tsx +55 -0
  20. package/src/components/display/StarRatingView.tsx +70 -0
  21. package/src/components/feedback/EmptyStateView.tsx +93 -0
  22. package/src/components/feedback/ErrorStateView.tsx +75 -0
  23. package/src/components/feedback/LoadingView.tsx +57 -0
  24. package/src/components/feedback/ProgressBar.tsx +42 -0
  25. package/src/components/feedback/PullToRefresh.tsx +38 -0
  26. package/src/components/feedback/ShimmerView.tsx +47 -0
  27. package/src/components/feedback/ToastView.tsx +71 -0
  28. package/src/components/inputs/CheckboxInput.tsx +73 -0
  29. package/src/components/inputs/Dropdown.tsx +110 -0
  30. package/src/components/inputs/QuantityStepper.tsx +81 -0
  31. package/src/components/inputs/RadioButtonInput.tsx +73 -0
  32. package/src/components/inputs/SearchInput.tsx +107 -0
  33. package/src/components/inputs/SecureInput.tsx +105 -0
  34. package/src/components/inputs/SegmentedControl.tsx +70 -0
  35. package/src/components/inputs/SliderInput.tsx +131 -0
  36. package/src/components/inputs/TextInput.tsx +90 -0
  37. package/src/components/inputs/ToggleSwitch.tsx +50 -0
  38. package/src/components/layout/KeyboardAvoidingScrollView.tsx +40 -0
  39. package/src/components/navigation/AppBar.tsx +53 -0
  40. package/src/components/navigation/BackButton.tsx +63 -0
  41. package/src/components/navigation/BottomNavigationBar.tsx +65 -0
  42. package/src/components/navigation/PagerView.tsx +40 -0
  43. package/src/components/navigation/TabBar.tsx +68 -0
  44. package/src/components/navigation/TabBarItem.ts +8 -0
  45. package/src/components/overlay/BottomSheet.tsx +64 -0
  46. package/src/components/overlay/CollapsibleView.tsx +82 -0
  47. package/src/components/overlay/ModalDialog.tsx +47 -0
  48. package/src/index.ts +54 -0
  49. package/src/tokens/colors.ts +84 -0
  50. package/src/tokens/index.ts +4 -0
  51. package/src/tokens/radius.ts +8 -0
  52. package/src/tokens/spacing.ts +10 -0
  53. package/src/tokens/typography.ts +27 -0
  54. 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
+ });