@startsimpli/ui-native 0.0.0-stage → 0.1.1
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 +174 -2
- package/package.json +60 -3
- package/src/__tests__/charts-math.test.ts +73 -0
- package/src/__tests__/styles-dashboard.test.ts +92 -0
- package/src/__tests__/styles-primitives.test.ts +114 -0
- package/src/__tests__/styles.test.ts +101 -0
- package/src/__tests__/theme.test.ts +23 -0
- package/src/__tests__/tokens.test.ts +33 -0
- package/src/charts-math.ts +106 -0
- package/src/components/AccentContext.tsx +22 -0
- package/src/components/Alert.tsx +27 -0
- package/src/components/Avatar.tsx +42 -0
- package/src/components/Badge.tsx +18 -0
- package/src/components/Button.tsx +40 -0
- package/src/components/Card.tsx +30 -0
- package/src/components/Checkbox.tsx +31 -0
- package/src/components/Chip.tsx +26 -0
- package/src/components/CompareChart.tsx +56 -0
- package/src/components/DataTable.tsx +89 -0
- package/src/components/DemographicBars.tsx +50 -0
- package/src/components/DonutChart.tsx +57 -0
- package/src/components/DonutLegend.tsx +49 -0
- package/src/components/DualRangeControl.tsx +31 -0
- package/src/components/EmptyState.tsx +63 -0
- package/src/components/ErrorBoundary.tsx +52 -0
- package/src/components/Flag.tsx +26 -0
- package/src/components/GradientBox.tsx +48 -0
- package/src/components/HeroBanner.tsx +160 -0
- package/src/components/Input.tsx +33 -0
- package/src/components/Label.tsx +9 -0
- package/src/components/ListRow.tsx +44 -0
- package/src/components/MetricRow.tsx +48 -0
- package/src/components/MultiLineLegend.tsx +30 -0
- package/src/components/Progress.tsx +18 -0
- package/src/components/Screen.tsx +38 -0
- package/src/components/SegmentedControl.tsx +31 -0
- package/src/components/Separator.tsx +13 -0
- package/src/components/ShareBar.tsx +32 -0
- package/src/components/Skeleton.tsx +29 -0
- package/src/components/SkeletonList.tsx +23 -0
- package/src/components/Sparkline.tsx +38 -0
- package/src/components/Spinner.tsx +8 -0
- package/src/components/StatTile.tsx +69 -0
- package/src/components/Switch.tsx +17 -0
- package/src/components/TabBar.tsx +50 -0
- package/src/components/Text.tsx +13 -0
- package/src/components/ThemeProvider.tsx +25 -0
- package/src/components/Timeline.tsx +47 -0
- package/src/components/TrendCard.tsx +39 -0
- package/src/components/TrendChart.tsx +68 -0
- package/src/index.ts +64 -0
- package/src/styles.ts +479 -0
- package/src/theme.ts +46 -0
- package/src/tokens.ts +145 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
import { Chip } from './Chip';
|
|
3
|
+
|
|
4
|
+
export interface SegmentedOption<T> {
|
|
5
|
+
value: T;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SegmentedControlProps<T extends string | number> {
|
|
10
|
+
options: SegmentedOption<T>[];
|
|
11
|
+
value: T;
|
|
12
|
+
onChange: (value: T) => void;
|
|
13
|
+
style?: StyleProp<ViewStyle>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Mutually-exclusive option selector rendered as a row of Chips. Generic over
|
|
17
|
+
* the value type — time ranges, status filters, category tabs, etc. */
|
|
18
|
+
export function SegmentedControl<T extends string | number>({
|
|
19
|
+
options,
|
|
20
|
+
value,
|
|
21
|
+
onChange,
|
|
22
|
+
style,
|
|
23
|
+
}: SegmentedControlProps<T>) {
|
|
24
|
+
return (
|
|
25
|
+
<View style={[{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }, style]}>
|
|
26
|
+
{options.map((o) => (
|
|
27
|
+
<Chip key={String(o.value)} label={o.label} selected={o.value === value} onPress={() => onChange(o.value)} />
|
|
28
|
+
))}
|
|
29
|
+
</View>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { View, type ViewProps } from 'react-native';
|
|
2
|
+
import { useTheme } from './ThemeProvider';
|
|
3
|
+
import { separatorStyle } from '../styles';
|
|
4
|
+
|
|
5
|
+
export interface SeparatorProps extends ViewProps {
|
|
6
|
+
orientation?: 'horizontal' | 'vertical';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Hairline divider. */
|
|
10
|
+
export function Separator({ orientation = 'horizontal', style, ...rest }: SeparatorProps) {
|
|
11
|
+
const theme = useTheme();
|
|
12
|
+
return <View style={[separatorStyle(theme, orientation), style]} {...rest} />;
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
import { useTheme } from './ThemeProvider';
|
|
3
|
+
import { shareBarTrackStyle } from '../styles';
|
|
4
|
+
|
|
5
|
+
export interface ShareSegment {
|
|
6
|
+
value: number;
|
|
7
|
+
color: string;
|
|
8
|
+
/** Optional, for accessibility / legends rendered by the caller. */
|
|
9
|
+
label?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ShareBarProps {
|
|
13
|
+
segments: ShareSegment[];
|
|
14
|
+
/** Bar thickness in px. */
|
|
15
|
+
height?: number;
|
|
16
|
+
style?: StyleProp<ViewStyle>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Horizontal segmented proportion bar (e.g. audience share by platform). Each
|
|
20
|
+
* segment's width is its value as a fraction of the total. */
|
|
21
|
+
export function ShareBar({ segments, height = 10, style }: ShareBarProps) {
|
|
22
|
+
const theme = useTheme();
|
|
23
|
+
const total = segments.reduce((sum, s) => sum + Math.max(0, s.value), 0);
|
|
24
|
+
return (
|
|
25
|
+
<View style={[shareBarTrackStyle(theme, height), style]} accessibilityRole="image">
|
|
26
|
+
{total > 0 &&
|
|
27
|
+
segments.map((s, i) => (
|
|
28
|
+
<View key={`${s.label ?? i}`} style={{ flex: Math.max(0, s.value) / total, backgroundColor: s.color }} />
|
|
29
|
+
))}
|
|
30
|
+
</View>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, type DimensionValue, type ViewStyle } from 'react-native';
|
|
3
|
+
import { useTheme } from './ThemeProvider';
|
|
4
|
+
import { skeletonStyle } from '../styles';
|
|
5
|
+
|
|
6
|
+
export interface SkeletonProps {
|
|
7
|
+
width?: DimensionValue;
|
|
8
|
+
height?: number;
|
|
9
|
+
style?: ViewStyle;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Pulsing placeholder for loading content. Mirrors @startsimpli/ui skeleton. */
|
|
13
|
+
export function Skeleton({ width = '100%', height = 16, style }: SkeletonProps) {
|
|
14
|
+
const theme = useTheme();
|
|
15
|
+
const opacity = useRef(new Animated.Value(0.5)).current;
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const loop = Animated.loop(
|
|
19
|
+
Animated.sequence([
|
|
20
|
+
Animated.timing(opacity, { toValue: 1, duration: 700, useNativeDriver: true }),
|
|
21
|
+
Animated.timing(opacity, { toValue: 0.4, duration: 700, useNativeDriver: true }),
|
|
22
|
+
])
|
|
23
|
+
);
|
|
24
|
+
loop.start();
|
|
25
|
+
return () => loop.stop();
|
|
26
|
+
}, [opacity]);
|
|
27
|
+
|
|
28
|
+
return <Animated.View style={[skeletonStyle(theme), { width, height, opacity }, style]} />;
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
import { Skeleton } from './Skeleton';
|
|
3
|
+
import { useTheme } from './ThemeProvider';
|
|
4
|
+
|
|
5
|
+
export interface SkeletonListProps {
|
|
6
|
+
/** Number of placeholder blocks. */
|
|
7
|
+
count?: number;
|
|
8
|
+
/** Height of each block. */
|
|
9
|
+
height?: number;
|
|
10
|
+
gap?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A column of card-shaped Skeleton placeholders for loading data screens. */
|
|
14
|
+
export function SkeletonList({ count = 3, height = 84, gap }: SkeletonListProps) {
|
|
15
|
+
const theme = useTheme();
|
|
16
|
+
return (
|
|
17
|
+
<View style={{ gap: gap ?? theme.spacing.md }}>
|
|
18
|
+
{Array.from({ length: count }).map((_, i) => (
|
|
19
|
+
<Skeleton key={i} width="100%" height={height} />
|
|
20
|
+
))}
|
|
21
|
+
</View>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Svg, { Path } from 'react-native-svg';
|
|
2
|
+
import { linePath, areaPath } from '../charts-math';
|
|
3
|
+
|
|
4
|
+
export interface SparklineProps {
|
|
5
|
+
data: number[];
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
/** Stroke (and area fill) color. Defaults to a neutral; pass a brand color. */
|
|
9
|
+
color?: string;
|
|
10
|
+
strokeWidth?: number;
|
|
11
|
+
/** Draw a faint filled area under the line. */
|
|
12
|
+
fill?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Tiny inline trend line (no axes), for placing next to a metric. */
|
|
16
|
+
export function Sparkline({
|
|
17
|
+
data,
|
|
18
|
+
width = 72,
|
|
19
|
+
height = 24,
|
|
20
|
+
color = '#3B82F6',
|
|
21
|
+
strokeWidth = 2,
|
|
22
|
+
fill = true,
|
|
23
|
+
}: SparklineProps) {
|
|
24
|
+
const box = { width, height, pad: strokeWidth };
|
|
25
|
+
return (
|
|
26
|
+
<Svg width={width} height={height}>
|
|
27
|
+
{fill && <Path d={areaPath(data, box)} fill={color} opacity={0.15} />}
|
|
28
|
+
<Path
|
|
29
|
+
d={linePath(data, box)}
|
|
30
|
+
stroke={color}
|
|
31
|
+
strokeWidth={strokeWidth}
|
|
32
|
+
fill="none"
|
|
33
|
+
strokeLinecap="round"
|
|
34
|
+
strokeLinejoin="round"
|
|
35
|
+
/>
|
|
36
|
+
</Svg>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ActivityIndicator, type ActivityIndicatorProps } from 'react-native';
|
|
2
|
+
import { useTheme } from './ThemeProvider';
|
|
3
|
+
|
|
4
|
+
/** Activity indicator defaulting to the brand color. Mirrors @startsimpli/ui page-loader. */
|
|
5
|
+
export function Spinner({ color, ...rest }: ActivityIndicatorProps) {
|
|
6
|
+
const theme = useTheme();
|
|
7
|
+
return <ActivityIndicator color={color ?? theme.colors.primary} {...rest} />;
|
|
8
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { View, Text as RNText, type ViewProps } from 'react-native';
|
|
3
|
+
import { useTheme } from './ThemeProvider';
|
|
4
|
+
import { useAccent } from './AccentContext';
|
|
5
|
+
import {
|
|
6
|
+
statTileStyle,
|
|
7
|
+
statValueStyle,
|
|
8
|
+
statLabelStyle,
|
|
9
|
+
statDeltaColor,
|
|
10
|
+
type StatDelta,
|
|
11
|
+
} from '../styles';
|
|
12
|
+
|
|
13
|
+
export interface StatTileProps extends ViewProps {
|
|
14
|
+
/** Pre-formatted metric value, e.g. "1.2M" or "$48,000". */
|
|
15
|
+
value: string;
|
|
16
|
+
/** Caption below the value. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Optional pre-formatted change, e.g. "+12%". */
|
|
19
|
+
delta?: string;
|
|
20
|
+
/** Drives the delta color (up = primary, down = destructive). */
|
|
21
|
+
deltaDirection?: StatDelta;
|
|
22
|
+
/** Optional leading glyph/icon node (kept icon-library-agnostic). */
|
|
23
|
+
icon?: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Dashboard metric tile: big value, caption, optional trend + icon. */
|
|
27
|
+
export function StatTile({
|
|
28
|
+
value,
|
|
29
|
+
label,
|
|
30
|
+
delta,
|
|
31
|
+
deltaDirection = 'neutral',
|
|
32
|
+
icon,
|
|
33
|
+
style,
|
|
34
|
+
...rest
|
|
35
|
+
}: StatTileProps) {
|
|
36
|
+
const theme = useTheme();
|
|
37
|
+
const accent = useAccent();
|
|
38
|
+
return (
|
|
39
|
+
<View
|
|
40
|
+
style={[
|
|
41
|
+
statTileStyle(theme),
|
|
42
|
+
// A 3px accent stripe at the top ties every tile to the screen's
|
|
43
|
+
// active accent — picks up the artist colour automatically.
|
|
44
|
+
{ borderTopWidth: 3, borderTopColor: accent },
|
|
45
|
+
style,
|
|
46
|
+
]}
|
|
47
|
+
{...rest}
|
|
48
|
+
>
|
|
49
|
+
{(icon || delta) && (
|
|
50
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
51
|
+
<View>{icon}</View>
|
|
52
|
+
{delta ? (
|
|
53
|
+
<RNText
|
|
54
|
+
style={{
|
|
55
|
+
fontSize: theme.fontSizes.xs,
|
|
56
|
+
fontWeight: theme.fontWeights.semibold as never,
|
|
57
|
+
color: statDeltaColor(theme, deltaDirection),
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{delta}
|
|
61
|
+
</RNText>
|
|
62
|
+
) : null}
|
|
63
|
+
</View>
|
|
64
|
+
)}
|
|
65
|
+
<RNText style={statValueStyle(theme)}>{value}</RNText>
|
|
66
|
+
<RNText style={statLabelStyle(theme)}>{label}</RNText>
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Switch as RNSwitch, type SwitchProps as RNSwitchProps } from 'react-native';
|
|
2
|
+
import { useTheme } from './ThemeProvider';
|
|
3
|
+
|
|
4
|
+
export type SwitchProps = RNSwitchProps;
|
|
5
|
+
|
|
6
|
+
/** Themed toggle (uses the platform Switch with brand track color). */
|
|
7
|
+
export function Switch({ value, ...rest }: SwitchProps) {
|
|
8
|
+
const theme = useTheme();
|
|
9
|
+
return (
|
|
10
|
+
<RNSwitch
|
|
11
|
+
value={value}
|
|
12
|
+
trackColor={{ false: theme.colors.input, true: theme.colors.primary }}
|
|
13
|
+
ios_backgroundColor={theme.colors.input}
|
|
14
|
+
{...rest}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { View, Pressable, Text as RNText, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
|
+
import { useTheme } from './ThemeProvider';
|
|
5
|
+
import { tabBarStyle, tabItemStyle, tabLabelStyle } from '../styles';
|
|
6
|
+
|
|
7
|
+
export interface TabItem {
|
|
8
|
+
key: string;
|
|
9
|
+
label: string;
|
|
10
|
+
/**
|
|
11
|
+
* Optional icon renderer. Receives the active state and theme-resolved color
|
|
12
|
+
* so the app can supply any icon library (or emoji) without ui-native taking
|
|
13
|
+
* an icon dependency.
|
|
14
|
+
*/
|
|
15
|
+
icon?: (state: { active: boolean; color: string }) => ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TabBarProps {
|
|
19
|
+
items: TabItem[];
|
|
20
|
+
activeKey: string;
|
|
21
|
+
onChange: (key: string) => void;
|
|
22
|
+
style?: StyleProp<ViewStyle>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Bottom tab bar. Pads for the home-indicator safe area; active tab uses the
|
|
26
|
+
* primary color. Icon-library-agnostic — pass an `icon` renderer per item. */
|
|
27
|
+
export function TabBar({ items, activeKey, onChange, style }: TabBarProps) {
|
|
28
|
+
const theme = useTheme();
|
|
29
|
+
const insets = useSafeAreaInsets();
|
|
30
|
+
return (
|
|
31
|
+
<View style={[tabBarStyle(theme), { paddingBottom: insets.bottom + theme.spacing.xs }, style]}>
|
|
32
|
+
{items.map((item) => {
|
|
33
|
+
const active = item.key === activeKey;
|
|
34
|
+
const color = active ? theme.colors.primary : theme.colors.mutedForeground;
|
|
35
|
+
return (
|
|
36
|
+
<Pressable
|
|
37
|
+
key={item.key}
|
|
38
|
+
accessibilityRole="tab"
|
|
39
|
+
accessibilityState={{ selected: active }}
|
|
40
|
+
onPress={() => onChange(item.key)}
|
|
41
|
+
style={tabItemStyle(theme)}
|
|
42
|
+
>
|
|
43
|
+
{item.icon?.({ active, color })}
|
|
44
|
+
<RNText style={tabLabelStyle(theme, active)}>{item.label}</RNText>
|
|
45
|
+
</Pressable>
|
|
46
|
+
);
|
|
47
|
+
})}
|
|
48
|
+
</View>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Text as RNText, type TextProps as RNTextProps } from 'react-native';
|
|
2
|
+
import { useTheme } from './ThemeProvider';
|
|
3
|
+
import { textStyle, type TextVariant } from '../styles';
|
|
4
|
+
|
|
5
|
+
export interface TextProps extends RNTextProps {
|
|
6
|
+
variant?: TextVariant;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Themed text. variant controls size/weight/color (heading/body/muted/caption). */
|
|
10
|
+
export function Text({ variant = 'body', style, ...rest }: TextProps) {
|
|
11
|
+
const theme = useTheme();
|
|
12
|
+
return <RNText style={[textStyle(theme, variant), style]} {...rest} />;
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from 'react';
|
|
2
|
+
import { useColorScheme } from 'react-native';
|
|
3
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
4
|
+
import { lightTheme, darkTheme, type Theme } from '../theme';
|
|
5
|
+
|
|
6
|
+
const ThemeContext = createContext<Theme>(lightTheme);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Provides a Theme to the primitives (and a SafeAreaProvider so Screen can read
|
|
10
|
+
* insets). With no `theme` prop it follows the OS light/dark setting; pass
|
|
11
|
+
* `theme` to pin or customize.
|
|
12
|
+
*/
|
|
13
|
+
export function ThemeProvider({ theme, children }: { theme?: Theme; children: ReactNode }) {
|
|
14
|
+
const scheme = useColorScheme();
|
|
15
|
+
const resolved = theme ?? (scheme === 'dark' ? darkTheme : lightTheme);
|
|
16
|
+
return (
|
|
17
|
+
<ThemeContext.Provider value={resolved}>
|
|
18
|
+
<SafeAreaProvider>{children}</SafeAreaProvider>
|
|
19
|
+
</ThemeContext.Provider>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useTheme(): Theme {
|
|
24
|
+
return useContext(ThemeContext);
|
|
25
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { View, Text as RNText } from 'react-native';
|
|
3
|
+
import { useTheme } from './ThemeProvider';
|
|
4
|
+
import { timelineDotStyle, timelineConnectorStyle, listRowTitleStyle, listRowSubtitleStyle } from '../styles';
|
|
5
|
+
|
|
6
|
+
export interface TimelineItem {
|
|
7
|
+
key: string;
|
|
8
|
+
title: string;
|
|
9
|
+
/** Secondary line under the title (e.g. a date). */
|
|
10
|
+
meta?: string;
|
|
11
|
+
/** Marker dot color (defaults to theme.primary). */
|
|
12
|
+
color?: string;
|
|
13
|
+
/** Right-aligned content (e.g. an amount or badge). */
|
|
14
|
+
trailing?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TimelineProps {
|
|
18
|
+
items: TimelineItem[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Vertical timeline: colored marker dots joined by a connector rail, each with
|
|
22
|
+
* a title/meta and optional trailing content. Generic — events, payouts, etc. */
|
|
23
|
+
export function Timeline({ items }: TimelineProps) {
|
|
24
|
+
const theme = useTheme();
|
|
25
|
+
return (
|
|
26
|
+
<View>
|
|
27
|
+
{items.map((item, i) => {
|
|
28
|
+
const last = i === items.length - 1;
|
|
29
|
+
return (
|
|
30
|
+
<View key={item.key} style={{ flexDirection: 'row', gap: theme.spacing.md }}>
|
|
31
|
+
<View style={{ width: 12, alignItems: 'center' }}>
|
|
32
|
+
<View style={timelineDotStyle(theme, item.color)} />
|
|
33
|
+
{!last && <View style={timelineConnectorStyle(theme)} />}
|
|
34
|
+
</View>
|
|
35
|
+
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'flex-start', paddingBottom: last ? 0 : theme.spacing.lg }}>
|
|
36
|
+
<View style={{ flex: 1 }}>
|
|
37
|
+
<RNText style={listRowTitleStyle(theme)}>{item.title}</RNText>
|
|
38
|
+
{item.meta ? <RNText style={listRowSubtitleStyle(theme)}>{item.meta}</RNText> : null}
|
|
39
|
+
</View>
|
|
40
|
+
{item.trailing ? <View>{item.trailing}</View> : null}
|
|
41
|
+
</View>
|
|
42
|
+
</View>
|
|
43
|
+
);
|
|
44
|
+
})}
|
|
45
|
+
</View>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
import { Card, CardContent } from './Card';
|
|
3
|
+
import { Text } from './Text';
|
|
4
|
+
import { TrendChart } from './TrendChart';
|
|
5
|
+
|
|
6
|
+
export interface TrendCardProps {
|
|
7
|
+
/** Small label, top-left (e.g. "Streams · 28 days"). */
|
|
8
|
+
caption: string;
|
|
9
|
+
/** Headline value, top-right (pre-formatted). */
|
|
10
|
+
value: string;
|
|
11
|
+
data: number[];
|
|
12
|
+
color?: string;
|
|
13
|
+
height?: number;
|
|
14
|
+
/** [start, end] captions under the chart. */
|
|
15
|
+
labels?: [string, string];
|
|
16
|
+
style?: StyleProp<ViewStyle>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A card with a caption + headline value over an area trend chart. The common
|
|
20
|
+
* "metric over time" block (reach, engagement, per-platform streams, …). */
|
|
21
|
+
export function TrendCard({ caption, value, data, color, height = 120, labels, style }: TrendCardProps) {
|
|
22
|
+
return (
|
|
23
|
+
<Card style={style}>
|
|
24
|
+
<CardContent>
|
|
25
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'baseline' }}>
|
|
26
|
+
<Text variant="caption">{caption}</Text>
|
|
27
|
+
<Text variant="body" style={{ fontWeight: '700' }}>
|
|
28
|
+
{value}
|
|
29
|
+
</Text>
|
|
30
|
+
</View>
|
|
31
|
+
{data.length > 0 && (
|
|
32
|
+
<View style={{ marginTop: 10 }}>
|
|
33
|
+
<TrendChart data={data} height={height} color={color} labels={labels} />
|
|
34
|
+
</View>
|
|
35
|
+
)}
|
|
36
|
+
</CardContent>
|
|
37
|
+
</Card>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { View, Text as RNText, type LayoutChangeEvent } from 'react-native';
|
|
3
|
+
import Svg, { Path, Line, Defs, LinearGradient, Stop } from 'react-native-svg';
|
|
4
|
+
import { useTheme } from './ThemeProvider';
|
|
5
|
+
import { linePath, areaPath } from '../charts-math';
|
|
6
|
+
|
|
7
|
+
export interface TrendChartProps {
|
|
8
|
+
data: number[];
|
|
9
|
+
height?: number;
|
|
10
|
+
color?: string;
|
|
11
|
+
/** Number of horizontal gridlines. */
|
|
12
|
+
gridLines?: number;
|
|
13
|
+
/** [start, end] captions under the chart (e.g. dates). */
|
|
14
|
+
labels?: [string, string];
|
|
15
|
+
strokeWidth?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Full-width area+line trend chart with faint gridlines. Measures its own
|
|
19
|
+
* width via onLayout so it fills the parent. */
|
|
20
|
+
export function TrendChart({
|
|
21
|
+
data,
|
|
22
|
+
height = 130,
|
|
23
|
+
color = '#3B82F6',
|
|
24
|
+
gridLines = 3,
|
|
25
|
+
labels,
|
|
26
|
+
strokeWidth = 2,
|
|
27
|
+
}: TrendChartProps) {
|
|
28
|
+
const theme = useTheme();
|
|
29
|
+
const [width, setWidth] = useState(0);
|
|
30
|
+
const onLayout = (e: LayoutChangeEvent) => setWidth(e.nativeEvent.layout.width);
|
|
31
|
+
const chartH = labels ? height - 18 : height;
|
|
32
|
+
const box = { width, height: chartH, pad: strokeWidth + 1 };
|
|
33
|
+
const gradId = 'trendFill';
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<View onLayout={onLayout}>
|
|
37
|
+
{width > 0 && (
|
|
38
|
+
<Svg width={width} height={chartH}>
|
|
39
|
+
<Defs>
|
|
40
|
+
<LinearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
|
|
41
|
+
<Stop offset="0" stopColor={color} stopOpacity={0.35} />
|
|
42
|
+
<Stop offset="1" stopColor={color} stopOpacity={0.02} />
|
|
43
|
+
</LinearGradient>
|
|
44
|
+
</Defs>
|
|
45
|
+
{Array.from({ length: gridLines }).map((_, i) => {
|
|
46
|
+
const y = (chartH / (gridLines + 1)) * (i + 1);
|
|
47
|
+
return <Line key={i} x1={0} y1={y} x2={width} y2={y} stroke={theme.colors.border} strokeWidth={1} />;
|
|
48
|
+
})}
|
|
49
|
+
<Path d={areaPath(data, box)} fill={`url(#${gradId})`} />
|
|
50
|
+
<Path
|
|
51
|
+
d={linePath(data, box)}
|
|
52
|
+
stroke={color}
|
|
53
|
+
strokeWidth={strokeWidth}
|
|
54
|
+
fill="none"
|
|
55
|
+
strokeLinecap="round"
|
|
56
|
+
strokeLinejoin="round"
|
|
57
|
+
/>
|
|
58
|
+
</Svg>
|
|
59
|
+
)}
|
|
60
|
+
{labels && (
|
|
61
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 4 }}>
|
|
62
|
+
<RNText style={{ fontSize: theme.fontSizes.xs, color: theme.colors.mutedForeground }}>{labels[0]}</RNText>
|
|
63
|
+
<RNText style={{ fontSize: theme.fontSizes.xs, color: theme.colors.mutedForeground }}>{labels[1]}</RNText>
|
|
64
|
+
</View>
|
|
65
|
+
)}
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @startsimpli/ui-native — React Native UI primitives fed by shared design
|
|
3
|
+
* tokens. The native counterpart to @startsimpli/ui (web/Tailwind); both draw
|
|
4
|
+
* from the same token values (see ./tokens and packages/ui/theme/contract.css).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Design tokens + resolved themes + pure style builders
|
|
8
|
+
export * from './tokens';
|
|
9
|
+
export * from './theme';
|
|
10
|
+
export * from './styles';
|
|
11
|
+
|
|
12
|
+
// Theming + layout
|
|
13
|
+
export { ThemeProvider, useTheme } from './components/ThemeProvider';
|
|
14
|
+
export { Screen, type ScreenProps } from './components/Screen';
|
|
15
|
+
export { ErrorBoundary, type ErrorBoundaryProps } from './components/ErrorBoundary';
|
|
16
|
+
|
|
17
|
+
// Primitives
|
|
18
|
+
export { Text, type TextProps } from './components/Text';
|
|
19
|
+
export { Button, type ButtonProps } from './components/Button';
|
|
20
|
+
export { Badge, type BadgeProps } from './components/Badge';
|
|
21
|
+
export { Label } from './components/Label';
|
|
22
|
+
export { Input, type InputProps } from './components/Input';
|
|
23
|
+
export { Separator, type SeparatorProps } from './components/Separator';
|
|
24
|
+
export { Checkbox, type CheckboxProps } from './components/Checkbox';
|
|
25
|
+
export { Switch, type SwitchProps } from './components/Switch';
|
|
26
|
+
export { Progress, type ProgressProps } from './components/Progress';
|
|
27
|
+
export { Alert, type AlertProps } from './components/Alert';
|
|
28
|
+
export { Skeleton, type SkeletonProps } from './components/Skeleton';
|
|
29
|
+
export { SkeletonList, type SkeletonListProps } from './components/SkeletonList';
|
|
30
|
+
export { Spinner } from './components/Spinner';
|
|
31
|
+
export {
|
|
32
|
+
Card,
|
|
33
|
+
CardHeader,
|
|
34
|
+
CardTitle,
|
|
35
|
+
CardDescription,
|
|
36
|
+
CardContent,
|
|
37
|
+
} from './components/Card';
|
|
38
|
+
export { Avatar, type AvatarProps } from './components/Avatar';
|
|
39
|
+
export { Flag, type FlagProps } from './components/Flag';
|
|
40
|
+
export { StatTile, type StatTileProps } from './components/StatTile';
|
|
41
|
+
export { EmptyState, type EmptyStateProps } from './components/EmptyState';
|
|
42
|
+
export { ListRow, type ListRowProps } from './components/ListRow';
|
|
43
|
+
export { MetricRow, type MetricRowProps } from './components/MetricRow';
|
|
44
|
+
export { TabBar, type TabBarProps, type TabItem } from './components/TabBar';
|
|
45
|
+
export { ShareBar, type ShareBarProps, type ShareSegment } from './components/ShareBar';
|
|
46
|
+
export { Chip, type ChipProps } from './components/Chip';
|
|
47
|
+
export { SegmentedControl, type SegmentedControlProps, type SegmentedOption } from './components/SegmentedControl';
|
|
48
|
+
export { Timeline, type TimelineProps, type TimelineItem } from './components/Timeline';
|
|
49
|
+
|
|
50
|
+
// Charts (require the optional react-native-svg peer dependency)
|
|
51
|
+
export * from './charts-math';
|
|
52
|
+
export { Sparkline, type SparklineProps } from './components/Sparkline';
|
|
53
|
+
export { DonutChart, type DonutChartProps } from './components/DonutChart';
|
|
54
|
+
export { DonutLegend, type DonutLegendProps, type DonutLegendItem } from './components/DonutLegend';
|
|
55
|
+
export { TrendChart, type TrendChartProps } from './components/TrendChart';
|
|
56
|
+
export { TrendCard, type TrendCardProps } from './components/TrendCard';
|
|
57
|
+
export { CompareChart, type CompareChartProps, type CompareSeries } from './components/CompareChart';
|
|
58
|
+
export { DemographicBars, type DemographicBarsProps, type DemographicBar } from './components/DemographicBars';
|
|
59
|
+
export { MultiLineLegend, type MultiLineLegendProps, type MultiLineLegendItem } from './components/MultiLineLegend';
|
|
60
|
+
export { DualRangeControl, type DualRangeControlProps } from './components/DualRangeControl';
|
|
61
|
+
export { GradientBox, type GradientBoxProps } from './components/GradientBox';
|
|
62
|
+
export { HeroBanner, type HeroBannerProps } from './components/HeroBanner';
|
|
63
|
+
export { AccentProvider, useAccent } from './components/AccentContext';
|
|
64
|
+
export { DataTable, type DataTableProps, type DataColumn, type DataRow } from './components/DataTable';
|