From 2da9964e49e64c02767342d2de675b776e8e6cda Mon Sep 17 00:00:00 2001
From: Ariel Costas Guerrero <ariel@costas.dev>
Date: Mon, 13 Oct 2025 00:14:56 +0200
Subject: Use openfreemapinstead of self-hosting, improve stop display, improve
 dark mode

---
 src/frontend/app/AppContext.tsx | 70 +++++++++++++++++++++++++++++++++++------
 1 file changed, 61 insertions(+), 9 deletions(-)

(limited to 'src/frontend/app/AppContext.tsx')

diff --git a/src/frontend/app/AppContext.tsx b/src/frontend/app/AppContext.tsx
index e6d8971..9013463 100644
--- a/src/frontend/app/AppContext.tsx
+++ b/src/frontend/app/AppContext.tsx
@@ -8,7 +8,7 @@ import {
 } from "react";
 import { type LngLatLike } from "maplibre-gl";
 
-type Theme = "light" | "dark";
+export type Theme = "light" | "dark" | "system";
 type TableStyle = "regular" | "grouped";
 type MapPositionMode = "gps" | "last";
 
@@ -47,23 +47,75 @@ const AppContext = createContext<AppContextProps | undefined>(undefined);
 
 export const AppProvider = ({ children }: { children: ReactNode }) => {
   //#region Theme
+  const getPreferredScheme = () => {
+    if (typeof window === "undefined" || !window.matchMedia) {
+      return "light" as const;
+    }
+    return window.matchMedia("(prefers-color-scheme: dark)").matches
+      ? "dark"
+      : "light";
+  };
+
+  const [systemTheme, setSystemTheme] = useState<"light" | "dark">(
+    getPreferredScheme,
+  );
+
   const [theme, setTheme] = useState<Theme>(() => {
     const savedTheme = localStorage.getItem("theme");
-    if (savedTheme) {
-      return savedTheme as Theme;
+    if (savedTheme === "light" || savedTheme === "dark" || savedTheme === "system") {
+      return savedTheme;
     }
-    const prefersDark =
-      window.matchMedia &&
-      window.matchMedia("(prefers-color-scheme: dark)").matches;
-    return prefersDark ? "dark" : "light";
+    return "system";
   });
 
+  useEffect(() => {
+    if (typeof window === "undefined" || !window.matchMedia) {
+      return;
+    }
+
+    const media = window.matchMedia("(prefers-color-scheme: dark)");
+    const handleChange = (event: MediaQueryListEvent) => {
+      setSystemTheme(event.matches ? "dark" : "light");
+    };
+
+    // Sync immediately in case theme changed before subscription
+    setSystemTheme(media.matches ? "dark" : "light");
+
+    if (media.addEventListener) {
+      media.addEventListener("change", handleChange);
+    } else {
+      media.addListener(handleChange);
+    }
+
+    return () => {
+      if (media.removeEventListener) {
+        media.removeEventListener("change", handleChange);
+      } else {
+        media.removeListener(handleChange);
+      }
+    };
+  }, []);
+
+  const resolvedTheme = theme === "system" ? systemTheme : theme;
+
   const toggleTheme = () => {
-    setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
+    setTheme((prevTheme) => {
+      if (prevTheme === "light") {
+        return "dark";
+      }
+      if (prevTheme === "dark") {
+        return "system";
+      }
+      return "light";
+    });
   };
 
   useEffect(() => {
-    document.documentElement.setAttribute("data-theme", theme);
+    document.documentElement.setAttribute("data-theme", resolvedTheme);
+    document.documentElement.style.colorScheme = resolvedTheme;
+  }, [resolvedTheme]);
+
+  useEffect(() => {
     localStorage.setItem("theme", theme);
   }, [theme]);
   //#endregion
-- 
cgit v1.3

