Skip to content

Commit cfc7080

Browse files
committed
Merge branch 'feat/app-espresso-design-system' into dev
Brings the Espresso design system into the app alongside the screenshot share flow, which were developed in parallel and could not both ship otherwise. The share confirm screen was written against the old theme, whose API this branch replaces (`useTheme().colors.card` becomes `useTheme().color.surface.raised`), so it is rewritten against the new tokens and the UI kit — Screen, TextField, Button, InlineAlert — rather than restyling raw React Native primitives. A screen that composes those stays on the palette when the palette changes; one that ships its own StyleSheet of colour literals does not. The root layout keeps both sides: the ThemeProvider wrapping ThemedStack, and the share-intent listener that routes an incoming share to the confirm screen.
2 parents a9ba8ef + f06fff3 commit cfc7080

40 files changed

Lines changed: 3320 additions & 1996 deletions

app/app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"expo": {
33
"name": "chronicle",
44
"slug": "friend-lite-app",
5-
"version": "1.0.11",
5+
"version": "1.0.12",
66
"scheme": "chronicle",
77
"orientation": "portrait",
88
"icon": "./assets/icon.png",

app/app/_layout.tsx

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,39 @@
1-
import { useEffect } from "react";
2-
import { Stack, useRouter } from "expo-router";
3-
import { useShareIntent } from "expo-share-intent";
4-
import { useTheme } from "@/theme";
5-
import { ConnectionLogProvider } from "@/contexts/ConnectionLogContext";
6-
import { AppSettingsProvider } from "@/contexts/AppSettingsContext";
7-
import ErrorBoundary from "@/components/ErrorBoundary";
8-
import { initLogger, logInfo } from "@/utils/logger";
1+
import { useEffect } from 'react';
2+
import { Stack, useRouter } from 'expo-router';
3+
import { useShareIntent } from 'expo-share-intent';
4+
5+
import ErrorBoundary from '@/components/ErrorBoundary';
6+
import { AppSettingsProvider } from '@/contexts/AppSettingsContext';
7+
import { ConnectionLogProvider } from '@/contexts/ConnectionLogContext';
8+
import { ThemeProvider, useTheme } from '@/theme';
9+
import { initLogger, logInfo } from '@/utils/logger';
10+
11+
/**
12+
* The navigator, split out because `useTheme()` requires a `ThemeProvider`
13+
* above it — the component that renders the provider cannot also consume it.
14+
*/
15+
function ThemedStack() {
16+
const t = useTheme();
17+
18+
return (
19+
<Stack
20+
screenOptions={{
21+
headerStyle: { backgroundColor: t.color.surface.raised },
22+
headerTintColor: t.color.text.primary,
23+
headerTitleStyle: { fontWeight: t.weight.semibold },
24+
contentStyle: { backgroundColor: t.color.surface.page },
25+
}}
26+
>
27+
<Stack.Screen name="index" options={{ title: 'Chronicle', headerShown: false }} />
28+
<Stack.Screen name="diagnostics" options={{ title: 'Diagnostics', presentation: 'card' }} />
29+
<Stack.Screen name="settings" options={{ title: 'Settings', presentation: 'card' }} />
30+
<Stack.Screen name="share" options={{ title: 'Add to Chronicle', presentation: 'modal' }} />
31+
<Stack.Screen name="theme-preview" options={{ title: 'Design System', presentation: 'card' }} />
32+
</Stack>
33+
);
34+
}
935

1036
export default function RootLayout() {
11-
const { colors, isDark } = useTheme();
1237
const router = useRouter();
1338
// Listening here rather than on the home screen so a share opens the confirm
1439
// sheet even when the app was launched cold straight into another route.
@@ -26,24 +51,15 @@ export default function RootLayout() {
2651
}, [hasShareIntent, router]);
2752

2853
return (
29-
<ErrorBoundary>
30-
<ConnectionLogProvider>
31-
<AppSettingsProvider>
32-
<Stack
33-
screenOptions={{
34-
headerStyle: { backgroundColor: colors.card },
35-
headerTintColor: colors.text,
36-
headerTitleStyle: { fontWeight: '600' },
37-
contentStyle: { backgroundColor: colors.background },
38-
}}
39-
>
40-
<Stack.Screen name="index" options={{ title: 'Chronicle', headerShown: false }} />
41-
<Stack.Screen name="diagnostics" options={{ title: 'Diagnostics', presentation: 'card' }} />
42-
<Stack.Screen name="settings" options={{ title: 'Settings', presentation: 'card' }} />
43-
<Stack.Screen name="share" options={{ title: 'Add to Chronicle', presentation: 'modal' }} />
44-
</Stack>
45-
</AppSettingsProvider>
46-
</ConnectionLogProvider>
47-
</ErrorBoundary>
54+
// The provider sits outside the boundary so the crash screen is themed too.
55+
<ThemeProvider>
56+
<ErrorBoundary>
57+
<ConnectionLogProvider>
58+
<AppSettingsProvider>
59+
<ThemedStack />
60+
</AppSettingsProvider>
61+
</ConnectionLogProvider>
62+
</ErrorBoundary>
63+
</ThemeProvider>
4864
);
4965
}

app/app/diagnostics.tsx

Lines changed: 101 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,100 @@
11
import React from 'react';
2-
import { View, Text, FlatList, TouchableOpacity, StyleSheet, SafeAreaView, Share, Platform, Alert } from 'react-native';
3-
import { useTheme, ThemeColors } from '@/theme';
2+
import { View, Text, FlatList, StyleSheet, SafeAreaView, Share, Platform, Alert } from 'react-native';
3+
import { useTheme, type Theme } from '@/theme';
4+
import { Badge, Button, Heading, Mono, type Tone } from '@/components/ui';
45
import { useConnectionLog, ConnectionEvent, ConnectionEventType } from '@/contexts/ConnectionLogContext';
56
import { getLogPath, readLog, clearLog } from '@/utils/logger';
67

7-
const EVENT_BADGE_COLORS: Record<ConnectionEventType, string> = {
8-
scan_start: '#007AFF',
9-
scan_stop: '#8E8E93',
10-
scan_result: '#5856D6',
11-
connect_start: '#FF9500',
12-
connect_success: '#34C759',
13-
connect_fail: '#FF3B30',
14-
disconnect: '#FF3B30',
15-
battery_read: '#34C759',
16-
audio_start: '#007AFF',
17-
audio_stop: '#8E8E93',
18-
error: '#FF3B30',
19-
health_ping: '#34C759',
20-
reconnect_attempt: '#FF9500',
21-
reconnect_backoff: '#FF9500',
22-
bt_state_change: '#5856D6',
23-
ws_connecting: '#FF9500',
24-
ws_open: '#34C759',
25-
ws_close: '#FF3B30',
26-
ws_error: '#FF3B30',
27-
ws_reconnect: '#FF9500',
28-
ws_reauth: '#AF52DE',
29-
net_change: '#5856D6',
8+
const EVENT_BADGE_TONES: Record<ConnectionEventType, Tone> = {
9+
scan_start: 'info',
10+
scan_stop: 'neutral',
11+
scan_result: 'suggest',
12+
connect_start: 'warning',
13+
connect_success: 'success',
14+
connect_fail: 'danger',
15+
disconnect: 'danger',
16+
battery_read: 'success',
17+
audio_start: 'info',
18+
audio_stop: 'neutral',
19+
error: 'danger',
20+
health_ping: 'success',
21+
reconnect_attempt: 'warning',
22+
reconnect_backoff: 'warning',
23+
bt_state_change: 'suggest',
24+
ws_connecting: 'warning',
25+
ws_open: 'success',
26+
ws_close: 'danger',
27+
ws_error: 'danger',
28+
ws_reconnect: 'warning',
29+
ws_reauth: 'suggest',
30+
net_change: 'suggest',
3031
};
3132

3233
function formatTime(date: Date): string {
3334
return date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
3435
}
3536

36-
function EventItem({ event, colors }: { event: ConnectionEvent; colors: ThemeColors }) {
37-
const badgeColor = EVENT_BADGE_COLORS[event.type] || colors.textTertiary;
37+
function EventItem({ event, t }: { event: ConnectionEvent; t: Theme }) {
38+
const s = createItemStyles(t);
39+
const badgeTone = EVENT_BADGE_TONES[event.type] ?? 'neutral';
3840

3941
return (
40-
<View style={[itemStyles.row, { borderBottomColor: colors.separator }]}>
41-
<Text style={[itemStyles.time, { color: colors.textTertiary }]}>{formatTime(event.timestamp)}</Text>
42-
<View style={[itemStyles.badge, { backgroundColor: badgeColor }]}>
43-
<Text style={itemStyles.badgeText}>{event.type.replace(/_/g, ' ')}</Text>
44-
</View>
45-
<View style={itemStyles.details}>
46-
{event.deviceName && <Text style={[itemStyles.device, { color: colors.text }]}>{event.deviceName}</Text>}
47-
{event.details && <Text style={[itemStyles.detail, { color: colors.textSecondary }]} numberOfLines={2}>{event.details}</Text>}
48-
{event.rssi != null && <Text style={[itemStyles.detail, { color: colors.textTertiary }]}>RSSI: {event.rssi} dBm</Text>}
42+
<View style={s.row}>
43+
<Mono style={s.time}>{formatTime(event.timestamp)}</Mono>
44+
<Badge tone={badgeTone} mono style={s.badge}>{event.type.replace(/_/g, ' ')}</Badge>
45+
<View style={s.details}>
46+
{event.deviceName && <Text style={s.device}>{event.deviceName}</Text>}
47+
{event.details && <Text style={s.detail} numberOfLines={2}>{event.details}</Text>}
48+
{event.rssi != null && <Text style={s.detailMuted}>RSSI: {event.rssi} dBm</Text>}
4949
</View>
5050
</View>
5151
);
5252
}
5353

54-
const itemStyles = StyleSheet.create({
54+
const createItemStyles = (t: Theme) => StyleSheet.create({
5555
row: {
5656
flexDirection: 'row',
5757
alignItems: 'flex-start',
58-
paddingVertical: 8,
59-
paddingHorizontal: 12,
60-
borderBottomWidth: 1,
58+
paddingVertical: t.space[2],
59+
paddingHorizontal: t.space[3],
60+
borderBottomWidth: t.borderWidth,
61+
borderBottomColor: t.color.border.subtle,
6162
},
6263
time: {
63-
fontSize: 11,
64-
fontFamily: 'monospace',
64+
fontFamily: t.font.mono,
6565
width: 65,
6666
marginTop: 3,
6767
},
6868
badge: {
69-
paddingHorizontal: 6,
70-
paddingVertical: 2,
71-
borderRadius: 4,
72-
marginRight: 8,
73-
marginTop: 2,
74-
},
75-
badgeText: {
76-
color: 'white',
77-
fontSize: 10,
78-
fontWeight: '600',
79-
textTransform: 'uppercase',
69+
marginRight: t.space[2],
70+
marginTop: t.space[0.5],
8071
},
8172
details: {
8273
flex: 1,
8374
},
8475
device: {
85-
fontSize: 13,
86-
fontWeight: '500',
76+
fontFamily: t.font.sans,
77+
...t.type.sm,
78+
fontWeight: t.weight.medium,
79+
color: t.color.text.primary,
8780
},
8881
detail: {
89-
fontSize: 12,
82+
fontFamily: t.font.sans,
83+
...t.type.xs,
84+
color: t.color.text.secondary,
85+
marginTop: 1,
86+
},
87+
detailMuted: {
88+
fontFamily: t.font.sans,
89+
...t.type.xs,
90+
color: t.color.text.muted,
9091
marginTop: 1,
9192
},
9293
});
9394

9495
export default function DiagnosticsScreen() {
95-
const { colors } = useTheme();
96+
const t = useTheme();
97+
const s = createScreenStyles(t);
9698
const { events, clearEvents } = useConnectionLog();
9799

98100
const shareLogFile = async () => {
@@ -120,102 +122,87 @@ export default function DiagnosticsScreen() {
120122
};
121123

122124
return (
123-
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
124-
<View style={[screenStyles.logBar, { borderBottomColor: colors.separator, backgroundColor: colors.card }]}>
125-
<Text style={[screenStyles.logBarTitle, { color: colors.text }]}>Crash Log</Text>
126-
<Text style={[screenStyles.logBarPath, { color: colors.textTertiary }]} numberOfLines={1}>{getLogPath()}</Text>
127-
<View style={screenStyles.logBarRow}>
128-
<TouchableOpacity onPress={shareLogFile} style={[screenStyles.logBtn, { backgroundColor: colors.inputBackground }]}>
129-
<Text style={[screenStyles.logBtnText, { color: colors.text }]}>Share Log File</Text>
130-
</TouchableOpacity>
131-
<TouchableOpacity onPress={wipeLogFile} style={[screenStyles.logBtn, { backgroundColor: colors.inputBackground }]}>
132-
<Text style={[screenStyles.logBtnText, { color: colors.danger }]}>Clear File</Text>
133-
</TouchableOpacity>
125+
<SafeAreaView style={s.safeArea}>
126+
<View style={s.logBar}>
127+
<Text style={s.logBarTitle}>Crash Log</Text>
128+
<Mono style={s.logBarPath} numberOfLines={1}>{getLogPath()}</Mono>
129+
<View style={s.logBarRow}>
130+
<Button variant="secondary" size="sm" style={s.logBtn} onPress={shareLogFile}>Share Log File</Button>
131+
<Button variant="danger" size="sm" style={s.logBtn} onPress={wipeLogFile}>Clear File</Button>
134132
</View>
135133
</View>
136-
<View style={[screenStyles.header, { borderBottomColor: colors.separator }]}>
137-
<Text style={[screenStyles.title, { color: colors.text }]}>Connection Log ({events.length})</Text>
138-
<TouchableOpacity onPress={clearEvents} style={[screenStyles.clearButton, { backgroundColor: colors.inputBackground }]}>
139-
<Text style={[screenStyles.clearText, { color: colors.danger }]}>Clear</Text>
140-
</TouchableOpacity>
134+
<View style={s.header}>
135+
<Heading>Connection Log ({events.length})</Heading>
136+
<Button variant="danger" size="sm" onPress={clearEvents}>Clear</Button>
141137
</View>
142138

143139
{events.length === 0 ? (
144-
<View style={screenStyles.empty}>
145-
<Text style={[screenStyles.emptyText, { color: colors.textTertiary }]}>No events recorded yet. Scan or connect a device to see events here.</Text>
140+
<View style={s.empty}>
141+
<Text style={s.emptyText}>No events recorded yet. Scan or connect a device to see events here.</Text>
146142
</View>
147143
) : (
148144
<FlatList
149145
data={events}
150-
renderItem={({ item }) => <EventItem event={item} colors={colors} />}
146+
renderItem={({ item }) => <EventItem event={item} t={t} />}
151147
keyExtractor={(item) => item.id}
152-
style={{ backgroundColor: colors.card }}
148+
style={s.list}
153149
/>
154150
)}
155151
</SafeAreaView>
156152
);
157153
}
158154

159-
const screenStyles = StyleSheet.create({
155+
const createScreenStyles = (t: Theme) => StyleSheet.create({
156+
safeArea: {
157+
flex: 1,
158+
backgroundColor: t.color.surface.page,
159+
},
160+
list: {
161+
backgroundColor: t.color.surface.raised,
162+
},
160163
header: {
161164
flexDirection: 'row',
162165
justifyContent: 'space-between',
163166
alignItems: 'center',
164-
paddingHorizontal: 16,
165-
paddingVertical: 12,
166-
borderBottomWidth: 1,
167-
},
168-
title: {
169-
fontSize: 17,
170-
fontWeight: '600',
171-
},
172-
clearButton: {
173-
paddingHorizontal: 12,
174-
paddingVertical: 6,
175-
borderRadius: 6,
176-
},
177-
clearText: {
178-
fontSize: 14,
179-
fontWeight: '500',
167+
paddingHorizontal: t.space[4],
168+
paddingVertical: t.space[3],
169+
borderBottomWidth: t.borderWidth,
170+
borderBottomColor: t.color.border.subtle,
180171
},
181172
empty: {
182173
flex: 1,
183174
justifyContent: 'center',
184175
alignItems: 'center',
185-
padding: 40,
176+
padding: t.space[10],
186177
},
187178
emptyText: {
188-
fontSize: 15,
179+
fontFamily: t.font.sans,
180+
...t.type.base,
181+
color: t.color.text.muted,
189182
textAlign: 'center',
190183
},
191184
logBar: {
192-
paddingHorizontal: 16,
193-
paddingVertical: 10,
194-
borderBottomWidth: 1,
185+
paddingHorizontal: t.space[4],
186+
paddingVertical: t.space[3],
187+
borderBottomWidth: t.borderWidth,
188+
borderBottomColor: t.color.border.subtle,
189+
backgroundColor: t.color.surface.raised,
195190
},
196191
logBarTitle: {
197-
fontSize: 15,
198-
fontWeight: '600',
192+
fontFamily: t.font.sans,
193+
...t.type.base,
194+
fontWeight: t.weight.semibold,
195+
color: t.color.text.primary,
199196
},
200197
logBarPath: {
201-
fontSize: 10,
202-
fontFamily: 'monospace',
203-
marginTop: 2,
204-
marginBottom: 8,
198+
marginTop: t.space[0.5],
199+
marginBottom: t.space[2],
205200
},
206201
logBarRow: {
207202
flexDirection: 'row',
208-
gap: 8,
203+
gap: t.space[2],
209204
},
210205
logBtn: {
211206
flex: 1,
212-
paddingHorizontal: 12,
213-
paddingVertical: 8,
214-
borderRadius: 6,
215-
alignItems: 'center',
216-
},
217-
logBtnText: {
218-
fontSize: 13,
219-
fontWeight: '500',
220207
},
221208
});

0 commit comments

Comments
 (0)