Skip to content

Commit 2afccf2

Browse files
author
Lalit Sharma
committed
feat: add theme settings screen and implement theme preference management
- Introduced ThemeSettingsScreen for user-selectable theme options (system, light, dark). - Integrated theme preference into app state management. - Updated TimerScreen to utilize theme colors for improved UI consistency. - Created color definitions for light and dark themes. - Implemented theme resolution logic based on user preference and system settings. - Added tests for theme resolution functionality. - Enhanced documentation to reflect new theme features and user feedback mechanisms.
1 parent aec17bc commit 2afccf2

18 files changed

Lines changed: 1805 additions & 1183 deletions

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.28] — 2026-02-26
9+
10+
### Added
11+
- Added a new `Settings` parent screen that links to `Theme Settings`, `Notification/Alarm Settings`, and `Location Settings`.
12+
- Added a dedicated `Theme Settings` screen with persisted `System`, `Light`, and `Dark` appearance options.
13+
- Added centralized mobile theme infrastructure (`apps/mobile/src/theme/colors.ts`, `apps/mobile/src/theme/resolveAppTheme.ts`, `apps/mobile/src/theme/useAppTheme.ts`) and app-state support for storing theme preference.
14+
- Added a theme-resolution regression test (`apps/mobile/tests/theme-resolution.test.ts`).
15+
- Added archived internal testing feedback report at `documents/reports/testing-feedback_2026-02-26.pdf`.
16+
17+
### Changed
18+
- Reorganized side-menu navigation to expose a single `Settings` destination, with notification/location settings moved under the parent settings flow.
19+
- Wired React Navigation container theming to app preference + system appearance and updated shared/shell styling (`BurgerButton`, `Landing`, `Timer`, `Notification/Alarm Settings`, `Location Settings`) to use theme tokens for light/dark support.
20+
- Updated Timer favorite empty-state guidance path to `Menu > Settings > Location Settings`.
21+
- Updated `documents/planning/tech-debt.md` with internal testing feedback mapping and new tracked backlog items (`ADD-11`..`ADD-15`, `IMP-28`..`IMP-29`).
22+
- Bumped `apps/mobile` version to `1.1.28`.
23+
24+
### Tests
25+
- Verified mobile checks pass after these changes: `pnpm -C apps/mobile typecheck`, `pnpm -C apps/mobile test`, and `pnpm -C apps/mobile lint`.
26+
827
## [1.1.27] — 2026-02-24
928

1029
### Fixed

apps/mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eclipse-timer/mobile",
3-
"version": "1.1.27",
3+
"version": "1.1.28",
44
"private": true,
55
"main": "index.js",
66
"scripts": {
Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import { useMemo } from "react";
12
import { Pressable, StyleSheet, View } from "react-native";
3+
import { useAppTheme } from "../theme/useAppTheme";
24

35
type BurgerButtonProps = {
46
onPress: () => void;
57
};
68

79
export default function BurgerButton({ onPress }: BurgerButtonProps) {
10+
const { colors } = useAppTheme();
11+
const styles = useMemo(() => createStyles(colors), [colors]);
12+
813
return (
914
<Pressable
1015
onPress={onPress}
@@ -19,22 +24,24 @@ export default function BurgerButton({ onPress }: BurgerButtonProps) {
1924
);
2025
}
2126

22-
const styles = StyleSheet.create({
23-
button: {
24-
width: 40,
25-
height: 40,
26-
borderRadius: 10,
27-
borderWidth: 1,
28-
borderColor: "#2f2f2f",
29-
backgroundColor: "#171717",
30-
alignItems: "center",
31-
justifyContent: "center",
32-
gap: 4,
33-
},
34-
line: {
35-
width: 18,
36-
height: 2,
37-
borderRadius: 2,
38-
backgroundColor: "white",
39-
},
40-
});
27+
function createStyles(colors: ReturnType<typeof useAppTheme>["colors"]) {
28+
return StyleSheet.create({
29+
button: {
30+
width: 40,
31+
height: 40,
32+
borderRadius: 10,
33+
borderWidth: 1,
34+
borderColor: colors.inputBorder,
35+
backgroundColor: colors.surfaceMuted,
36+
alignItems: "center",
37+
justifyContent: "center",
38+
gap: 4,
39+
},
40+
line: {
41+
width: 18,
42+
height: 2,
43+
borderRadius: 2,
44+
backgroundColor: colors.textPrimary,
45+
},
46+
});
47+
}

apps/mobile/src/navigation/RootNavigator.tsx

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import type {
1313
import {
1414
type LinkingOptions,
1515
NavigationContainer,
16+
DarkTheme as NavigationDarkTheme,
17+
DefaultTheme as NavigationLightTheme,
18+
type Theme as NavigationTheme,
1619
useFocusEffect,
1720
useIsFocused,
1821
useNavigationContainerRef,
@@ -44,10 +47,13 @@ import EclipsePreviewScreen, { type PreviewPayload } from "../screens/EclipsePre
4447
import LandingScreen from "../screens/LandingScreen";
4548
import LocationSettingsScreen from "../screens/LocationSettingsScreen";
4649
import NotificationSettingsScreen from "../screens/NotificationSettingsScreen";
50+
import SettingsScreen from "../screens/SettingsScreen";
51+
import ThemeSettingsScreen from "../screens/ThemeSettingsScreen";
4752
import TimerScreen from "../screens/TimerScreen";
4853
import { syncWearPreviewRouteState } from "../services/wearPreviewPublisher";
4954
import { startWearLiveSync } from "../services/wearSync";
5055
import { type FavoriteLocation, useAppState } from "../state/appState";
56+
import { useAppTheme } from "../theme/useAppTheme";
5157
import { localYmdNow } from "../utils/date";
5258
import { kindCodeForRecord, kindLabelFromCode } from "../utils/eclipse";
5359
import SideMenu, { type MenuRouteName } from "./SideMenu";
@@ -58,6 +64,8 @@ type RootStackParamList = {
5864
Landing: undefined;
5965
Timer: undefined;
6066
Preview: { payload: PreviewPayload };
67+
Settings: undefined;
68+
ThemeSettings: undefined;
6169
NotificationSettings: undefined;
6270
LocationSettings: undefined;
6371
};
@@ -68,6 +76,8 @@ const linking: LinkingOptions<RootStackParamList> = {
6876
screens: {
6977
Landing: "landing",
7078
Timer: "timer",
79+
Settings: "settings",
80+
ThemeSettings: "settings/theme",
7181
NotificationSettings: "notifications",
7282
LocationSettings: "locations",
7383
},
@@ -90,6 +100,14 @@ type PreviewRouteProps = NativeStackScreenProps<RootStackParamList, "Preview"> &
90100
onOpenMenu: () => void;
91101
};
92102

103+
type SettingsRouteProps = NativeStackScreenProps<RootStackParamList, "Settings"> & {
104+
onOpenMenu: () => void;
105+
};
106+
107+
type ThemeSettingsRouteProps = NativeStackScreenProps<RootStackParamList, "ThemeSettings"> & {
108+
onOpenMenu: () => void;
109+
};
110+
93111
type RouteWithMenuProps = {
94112
onOpenMenu: () => void;
95113
};
@@ -119,22 +137,43 @@ function filterLandingEclipses(
119137
});
120138
}
121139

122-
function StartupLoadingScreen({ message }: { message: string }) {
140+
function StartupLoadingScreen({
141+
message,
142+
colors,
143+
}: {
144+
message: string;
145+
colors: ReturnType<typeof useAppTheme>["colors"];
146+
}) {
123147
return (
124-
<View style={styles.startupSafe}>
125-
<View style={styles.startupCard}>
148+
<View style={[styles.startupSafe, { backgroundColor: colors.background }]}>
149+
<View
150+
style={[
151+
styles.startupCard,
152+
{
153+
backgroundColor: colors.surface,
154+
borderColor: colors.border,
155+
},
156+
]}
157+
>
126158
<Image source={APP_LOGO} style={styles.startupLogo} resizeMode="contain" />
127159
<ActivityIndicator />
128-
<Text style={styles.startupTitle}>Eclipse Timer</Text>
129-
<Text style={styles.startupSubtitle}>{message}</Text>
160+
<Text style={[styles.startupTitle, { color: colors.textPrimary }]}>Eclipse Timer</Text>
161+
<Text style={[styles.startupSubtitle, { color: colors.textMuted }]}>{message}</Text>
130162
</View>
131163
</View>
132164
);
133165
}
134166

135167
function toMenuRouteName(route: keyof RootStackParamList): MenuRouteName | null {
136168
if (route === "Landing" || route === "Timer") return route;
137-
if (route === "NotificationSettings" || route === "LocationSettings") return route;
169+
if (
170+
route === "Settings" ||
171+
route === "ThemeSettings" ||
172+
route === "NotificationSettings" ||
173+
route === "LocationSettings"
174+
) {
175+
return "Settings";
176+
}
138177
return null;
139178
}
140179

@@ -425,6 +464,29 @@ function PreviewRoute({ navigation, route, onOpenMenu }: PreviewRouteProps) {
425464
);
426465
}
427466

467+
function SettingsRoute({ navigation, onOpenMenu }: SettingsRouteProps) {
468+
return (
469+
<SettingsScreen
470+
onOpenMenu={onOpenMenu}
471+
onOpenThemeSettings={() => navigation.navigate("ThemeSettings")}
472+
onOpenNotificationSettings={() => navigation.navigate("NotificationSettings")}
473+
onOpenLocationSettings={() => navigation.navigate("LocationSettings")}
474+
/>
475+
);
476+
}
477+
478+
function ThemeSettingsRoute({ onOpenMenu }: ThemeSettingsRouteProps) {
479+
const { state, actions } = useAppState();
480+
481+
return (
482+
<ThemeSettingsScreen
483+
onOpenMenu={onOpenMenu}
484+
preference={state.themePreference}
485+
onSetThemePreference={actions.setThemePreference}
486+
/>
487+
);
488+
}
489+
428490
function NotificationSettingsRoute({ onOpenMenu }: RouteWithMenuProps) {
429491
const { state, actions } = useAppState();
430492

@@ -456,11 +518,33 @@ function LocationSettingsRoute({ onOpenMenu }: RouteWithMenuProps) {
456518

457519
export default function RootNavigator() {
458520
const { state: appState, actions } = useAppState();
521+
const { colors, resolvedTheme } = useAppTheme();
459522
const navigationRef = useNavigationContainerRef<RootStackParamList>();
460523
const pendingFeaturedDeepLinkActionRef = useRef<FeaturedEclipseDeepLinkAction | null>(null);
461524
const [catalog, setCatalog] = useState<EclipseRecord[] | null>(null);
462525
const [isMenuOpen, setIsMenuOpen] = useState(false);
463526
const [currentRouteName, setCurrentRouteName] = useState<keyof RootStackParamList>("Landing");
527+
const navigationTheme = useMemo<NavigationTheme>(() => {
528+
const base = resolvedTheme === "light" ? NavigationLightTheme : NavigationDarkTheme;
529+
return {
530+
...base,
531+
colors: {
532+
...base.colors,
533+
primary: colors.primary,
534+
background: colors.background,
535+
card: colors.surface,
536+
border: colors.border,
537+
text: colors.textPrimary,
538+
},
539+
};
540+
}, [
541+
colors.background,
542+
colors.border,
543+
colors.primary,
544+
colors.surface,
545+
colors.textPrimary,
546+
resolvedTheme,
547+
]);
464548

465549
useNotificationScheduler(
466550
appState.notificationSettings,
@@ -628,13 +712,14 @@ export default function RootNavigator() {
628712
}, [handleIncomingUrl]);
629713

630714
if (!catalog) {
631-
return <StartupLoadingScreen message="Loading eclipse catalog..." />;
715+
return <StartupLoadingScreen message="Loading eclipse catalog..." colors={colors} />;
632716
}
633717

634718
return (
635719
<NavigationContainer
636720
ref={navigationRef}
637721
linking={linking}
722+
theme={navigationTheme}
638723
onReady={onNavigationReady}
639724
onStateChange={syncWearPreviewWithRoute}
640725
>
@@ -649,6 +734,12 @@ export default function RootNavigator() {
649734
<Stack.Screen name="Preview">
650735
{(props) => <PreviewRoute {...props} onOpenMenu={openMenu} />}
651736
</Stack.Screen>
737+
<Stack.Screen name="Settings">
738+
{(props) => <SettingsRoute {...props} onOpenMenu={openMenu} />}
739+
</Stack.Screen>
740+
<Stack.Screen name="ThemeSettings">
741+
{(props) => <ThemeSettingsRoute {...props} onOpenMenu={openMenu} />}
742+
</Stack.Screen>
652743
<Stack.Screen name="NotificationSettings">
653744
{() => <NotificationSettingsRoute onOpenMenu={openMenu} />}
654745
</Stack.Screen>

0 commit comments

Comments
 (0)