Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { THEME_COLORS } from "@openmapx/core/theme";
import type { ExpoConfig } from "expo/config";
// Explicit `.ts` specifiers: Expo's config loader transpiles only this entry
// file, so its imports are resolved by Node, which needs the real extension to
Expand All @@ -14,8 +15,11 @@ import { IOS_USAGE_DESCRIPTIONS } from "./config/nativeCopy.ts";
*/
const mobile = readMobileConfig(process.env);

/** OpenMapX brand blue, used for the Android notification tint and splash. */
const BRAND_COLOR = "#1B69D6";
/**
* The web UI's primary colour, used for the Android notification tint and the
* adaptive icon's background, so the app's own surfaces match the page.
*/
const BRAND_COLOR = THEME_COLORS.light.primary;

/**
* Associated domains exist only when a real Apple Team ID is present, which
Expand Down Expand Up @@ -121,8 +125,16 @@ const config: ExpoConfig = {
image: "./assets/splash-icon.png",
imageWidth: 180,
resizeMode: "contain",
backgroundColor: "#FFFFFF",
dark: { backgroundColor: "#0B0F14" },
backgroundColor: THEME_COLORS.light.surface,
dark: { backgroundColor: THEME_COLORS.dark.background },
// Android 12 and later draw the splash image on a 288 dp canvas and
// crop it to a 192 dp circle, which cut the corners and edges off the
// square logo. This image already holds the whole canvas, with the
// logo small enough that its rounded corners stay inside the circle.
android: {
image: "./assets/splash-icon-android.png",
imageWidth: 288,
},
},
],
[
Expand Down
Binary file added apps/mobile/assets/splash-icon-android.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion apps/mobile/src/location/ExpoLocationDriver.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { THEME_COLORS } from "@openmapx/core/theme";
import * as Location from "expo-location";
import { Platform } from "react-native";
import { ANDROID_FOREGROUND_SERVICE_COPY, type MobileLocale } from "../../config/nativeCopy";
Expand Down Expand Up @@ -121,7 +122,7 @@ export class ExpoLocationDriver implements LocationDriver {
foregroundService: {
notificationTitle: ANDROID_FOREGROUND_SERVICE_COPY[this.locale].notificationTitle,
notificationBody: ANDROID_FOREGROUND_SERVICE_COPY[this.locale].notificationBody,
notificationColor: "#1B69D6",
notificationColor: THEME_COLORS.light.primary,
killServiceOnDestroy: false,
},
}
Expand Down
17 changes: 7 additions & 10 deletions apps/mobile/src/permissions/PermissionDisclosure.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type ReactElement, useMemo } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import type { MobileLocale } from "../../config/nativeCopy";
import { useNativePalette } from "../shell/nativeTheme";
import { BUTTON_SHAPE, useNativePalette } from "../shell/nativeTheme";
import { SCROLL_SAFE_AREA, topBarInset } from "../shell/systemBars";
import { permissionCopy } from "./permissionCopy";
import type { PermissionFlowState } from "./permissionMachine";
Expand All @@ -27,35 +27,32 @@ const styles = StyleSheet.create({
right: 0,
bottom: 0,
left: 0,
backgroundColor: "#FFFFFF",
},
content: { padding: 24, paddingBottom: 40 },
title: { fontSize: 22, fontWeight: "600", marginBottom: 16, color: "#101418" },
paragraph: { fontSize: 16, lineHeight: 23, marginBottom: 12, color: "#3C4650" },
title: { fontSize: 22, fontWeight: "600", marginBottom: 16 },
paragraph: { fontSize: 16, lineHeight: 23, marginBottom: 12 },
actions: { marginTop: 12 },
primary: {
minHeight: 48,
borderRadius: 10,
borderRadius: BUTTON_SHAPE.borderRadius,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 20,
paddingVertical: 12,
backgroundColor: "#1B69D6",
marginBottom: 12,
},
primaryLabel: { color: "#FFFFFF", fontSize: 16, fontWeight: "600" },
primaryLabel: { fontSize: 16, fontWeight: BUTTON_SHAPE.labelWeight },
secondary: {
minHeight: 48,
borderRadius: 10,
borderRadius: BUTTON_SHAPE.borderRadius,
borderWidth: 1,
borderColor: "#C3CBD3",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 20,
paddingVertical: 12,
marginBottom: 12,
},
secondaryLabel: { color: "#1B3A5C", fontSize: 16, fontWeight: "600" },
secondaryLabel: { fontSize: 16, fontWeight: BUTTON_SHAPE.labelWeight },
});

/** The static layout above, coloured for the current system appearance. */
Expand Down
7 changes: 4 additions & 3 deletions apps/mobile/src/shell/FeasibilityOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type FeasibilityProbeState,
FeasibilityRepository,
} from "../storage/feasibilityRepository";
import { BUTTON_SHAPE, DARK_PALETTE } from "./nativeTheme";
import { deviceMobileLocale } from "./shellCopy";

/**
Expand Down Expand Up @@ -42,10 +43,10 @@ const styles = StyleSheet.create({
minHeight: 44,
paddingHorizontal: 14,
justifyContent: "center",
borderRadius: 8,
backgroundColor: "#1B69D6",
borderRadius: BUTTON_SHAPE.borderRadius,
backgroundColor: DARK_PALETTE.accent,
},
buttonLabel: { color: "#FFFFFF", fontSize: 13, fontWeight: "600" },
buttonLabel: { color: DARK_PALETTE.onAccent, fontSize: 13, fontWeight: BUTTON_SHAPE.labelWeight },
});

const PROFILE_CYCLE: LocationProfileKind[] = ["driving", "walking", "transit-cruise"];
Expand Down
17 changes: 7 additions & 10 deletions apps/mobile/src/shell/NativeRecoveryOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactElement } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import type { MobileLocale } from "../../config/nativeCopy";
import { useNativePalette } from "./nativeTheme";
import { BUTTON_SHAPE, useNativePalette } from "./nativeTheme";
import { actionsFor, type ShellAction, type ShellState } from "./ShellState";
import { type ShellCopyKey, shellCopy } from "./shellCopy";
import { SCROLL_SAFE_AREA, topBarInset } from "./systemBars";
Expand All @@ -27,34 +27,31 @@ const styles = StyleSheet.create({
right: 0,
bottom: 0,
left: 0,
backgroundColor: "#FFFFFF",
},
content: { padding: 24, paddingBottom: 40, flexGrow: 1, justifyContent: "center" },
title: { fontSize: 21, fontWeight: "600", marginBottom: 12, color: "#101418" },
body: { fontSize: 16, lineHeight: 23, marginBottom: 24, color: "#3C4650" },
title: { fontSize: 21, fontWeight: "600", marginBottom: 12 },
body: { fontSize: 16, lineHeight: 23, marginBottom: 24 },
primary: {
minHeight: 48,
borderRadius: 10,
borderRadius: BUTTON_SHAPE.borderRadius,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 20,
paddingVertical: 12,
backgroundColor: "#1B69D6",
marginBottom: 12,
},
primaryLabel: { color: "#FFFFFF", fontSize: 16, fontWeight: "600" },
primaryLabel: { fontSize: 16, fontWeight: BUTTON_SHAPE.labelWeight },
secondary: {
minHeight: 48,
borderRadius: 10,
borderRadius: BUTTON_SHAPE.borderRadius,
borderWidth: 1,
borderColor: "#C3CBD3",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 20,
paddingVertical: 12,
marginBottom: 12,
},
secondaryLabel: { color: "#1B3A5C", fontSize: 16, fontWeight: "600" },
secondaryLabel: { fontSize: 16, fontWeight: BUTTON_SHAPE.labelWeight },
});

const COPY: Partial<Record<ShellState["kind"], { title: ShellCopyKey; body: ShellCopyKey }>> = {
Expand Down
12 changes: 4 additions & 8 deletions apps/mobile/src/shell/ShellOverlays.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactElement } from "react";
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from "react-native";
import type { MobileLocale } from "../../config/nativeCopy";
import { useNativePalette } from "./nativeTheme";
import { BUTTON_SHAPE, useNativePalette } from "./nativeTheme";
import { shellCopy } from "./shellCopy";

/**
Expand All @@ -22,34 +22,30 @@ const styles = StyleSheet.create({
alignItems: "center",
justifyContent: "center",
padding: 24,
backgroundColor: "#FFFFFF",
},
title: {
fontSize: 20,
fontWeight: "600",
textAlign: "center",
marginBottom: 8,
color: "#101418",
},
body: {
fontSize: 16,
lineHeight: 22,
textAlign: "center",
marginBottom: 24,
color: "#3C4650",
},
button: {
minHeight: 48,
minWidth: 160,
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: 10,
borderRadius: BUTTON_SHAPE.borderRadius,
alignItems: "center",
justifyContent: "center",
backgroundColor: "#1B69D6",
},
buttonLabel: { color: "#FFFFFF", fontSize: 16, fontWeight: "600" },
loadingLabel: { marginTop: 16, fontSize: 16, color: "#3C4650" },
buttonLabel: { fontSize: 16, fontWeight: BUTTON_SHAPE.labelWeight },
loadingLabel: { marginTop: 16, fontSize: 16 },
});

export function LoadingOverlay({ locale }: { locale: MobileLocale }): ReactElement {
Expand Down
30 changes: 30 additions & 0 deletions apps/mobile/src/shell/nativeTheme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { THEME_COLORS } from "@openmapx/core/theme";
import { DARK_PALETTE, LIGHT_PALETTE, paletteFor } from "./nativeTheme";

describe("native palette", () => {
it("draws the native screens in the web UI's colours", () => {
expect(LIGHT_PALETTE).toMatchObject({
background: THEME_COLORS.light.surface,
title: THEME_COLORS.light.text,
body: THEME_COLORS.light.textSecondary,
accent: THEME_COLORS.light.primary,
onAccent: THEME_COLORS.light.onPrimary,
secondaryLabel: THEME_COLORS.light.primary,
});
expect(DARK_PALETTE).toMatchObject({
background: THEME_COLORS.dark.background,
accent: THEME_COLORS.dark.primary,
onAccent: THEME_COLORS.dark.onPrimary,
});
});

it("outlines secondary buttons in the primary colour at half opacity", () => {
expect(LIGHT_PALETTE.outline).toBe("rgba(32, 126, 35, 0.5)");
});

it("picks the palette for the system appearance", () => {
expect(paletteFor("dark")).toBe(DARK_PALETTE);
expect(paletteFor("light")).toBe(LIGHT_PALETTE);
expect(paletteFor(null)).toBe(LIGHT_PALETTE);
});
});
61 changes: 37 additions & 24 deletions apps/mobile/src/shell/nativeTheme.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { THEME_COLORS, THEME_CORNER_RADIUS, type ThemeColors } from "@openmapx/core/theme";
import { useColorScheme } from "react-native";

/**
* Colours for the few native screens the shell draws itself.
*
* They follow the system appearance, like the splash screen and the web UI do
* by default. A white loading screen on a dark phone flashes between a dark
* splash and a dark map, and leaves the light status-bar icons unreadable.
* They come from the web UI's theme, so a native screen in front of the page
* looks like part of the same product, and they follow the system appearance
* like the splash screen and the web UI do by default. A white loading screen
* on a dark phone flashes between a dark splash and a dark map, and leaves the
* light status-bar icons unreadable.
*/
export interface NativePalette {
scheme: "light" | "dark";
Expand All @@ -14,32 +17,42 @@ export interface NativePalette {
body: string;
accent: string;
onAccent: string;
/** The border of a secondary (outlined) button. */
outline: string;
/** The label of a secondary (outlined) button. */
secondaryLabel: string;
}

export const LIGHT_PALETTE: NativePalette = {
scheme: "light",
background: "#FFFFFF",
title: "#101418",
body: "#3C4650",
accent: "#1B69D6",
onAccent: "#FFFFFF",
outline: "#C3CBD3",
secondaryLabel: "#1B3A5C",
};
/** `#RRGGBB` at the given opacity, as the web's outlined buttons draw their border. */
function withAlpha(hex: string, alpha: number): string {
const value = Number.parseInt(hex.slice(1), 16);
return `rgba(${(value >> 16) & 255}, ${(value >> 8) & 255}, ${value & 255}, ${alpha})`;
}

function paletteOf(scheme: "light" | "dark", colors: ThemeColors): NativePalette {
return {
scheme,
// Full-screen pages, not raised sheets: light pages are white like the
// web's full-screen dialogs, and dark ones match the splash and the web's
// dark status bar.
background: scheme === "light" ? colors.surface : colors.background,
title: colors.text,
body: colors.textSecondary,
accent: colors.primary,
onAccent: colors.onPrimary,
outline: withAlpha(colors.primary, 0.5),
secondaryLabel: colors.primary,
};
}

export const LIGHT_PALETTE: NativePalette = paletteOf("light", THEME_COLORS.light);
export const DARK_PALETTE: NativePalette = paletteOf("dark", THEME_COLORS.dark);

/** Matches the splash screen's dark background. */
export const DARK_PALETTE: NativePalette = {
scheme: "dark",
background: "#0B0F14",
title: "#E8ECF0",
body: "#B4BEC8",
accent: "#5B9BF0",
onAccent: "#0B0F14",
outline: "#3C4650",
secondaryLabel: "#B9D3F5",
};
/** Button shape and label weight, as the web's MUI theme draws them. */
export const BUTTON_SHAPE = {
borderRadius: THEME_CORNER_RADIUS,
labelWeight: "500",
} as const;

export function paletteFor(scheme: string | null | undefined): NativePalette {
return scheme === "dark" ? DARK_PALETTE : LIGHT_PALETTE;
Expand Down
Loading
Loading