diff options
Diffstat (limited to 'src/frontend/app')
| -rw-r--r-- | src/frontend/app/AppContext.tsx | 70 | ||||
| -rw-r--r-- | src/frontend/app/i18n/locales/en-GB.json | 5 | ||||
| -rw-r--r-- | src/frontend/app/i18n/locales/es-ES.json | 5 | ||||
| -rw-r--r-- | src/frontend/app/i18n/locales/gl-ES.json | 5 | ||||
| -rw-r--r-- | src/frontend/app/maps/styleloader.ts | 15 | ||||
| -rw-r--r-- | src/frontend/app/root.css | 3 | ||||
| -rw-r--r-- | src/frontend/app/routes/map.tsx | 26 | ||||
| -rw-r--r-- | src/frontend/app/routes/settings.css | 1 | ||||
| -rw-r--r-- | src/frontend/app/routes/settings.tsx | 8 |
9 files changed, 114 insertions, 24 deletions
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 diff --git a/src/frontend/app/i18n/locales/en-GB.json b/src/frontend/app/i18n/locales/en-GB.json index ee8ec7a..4d0c91b 100644 --- a/src/frontend/app/i18n/locales/en-GB.json +++ b/src/frontend/app/i18n/locales/en-GB.json @@ -9,8 +9,9 @@ "data_source_middle": "under license", "settings": "Settings", "theme": "Mode:", - "theme_light": "Light", - "theme_dark": "Dark", + "theme_light": "Light", + "theme_dark": "Dark", + "theme_system": "System", "table_style": "Table style:", "table_style_regular": "Show in order", "table_style_grouped": "Group by line", diff --git a/src/frontend/app/i18n/locales/es-ES.json b/src/frontend/app/i18n/locales/es-ES.json index a152564..0915361 100644 --- a/src/frontend/app/i18n/locales/es-ES.json +++ b/src/frontend/app/i18n/locales/es-ES.json @@ -9,8 +9,9 @@ "data_source_middle": "bajo licencia", "settings": "Ajustes", "theme": "Modo:", - "theme_light": "Claro", - "theme_dark": "Oscuro", + "theme_light": "Claro", + "theme_dark": "Oscuro", + "theme_system": "Sistema", "table_style": "Estilo de tabla:", "table_style_regular": "Mostrar por orden", "table_style_grouped": "Agrupar por línea", diff --git a/src/frontend/app/i18n/locales/gl-ES.json b/src/frontend/app/i18n/locales/gl-ES.json index b683fc0..8949333 100644 --- a/src/frontend/app/i18n/locales/gl-ES.json +++ b/src/frontend/app/i18n/locales/gl-ES.json @@ -9,8 +9,9 @@ "data_source_middle": "baixo licenza", "settings": "Axustes", "theme": "Modo:", - "theme_light": "Claro", - "theme_dark": "Escuro", + "theme_light": "Claro", + "theme_dark": "Escuro", + "theme_system": "Sistema", "table_style": "Estilo de táboa:", "table_style_regular": "Mostrar por orde", "table_style_grouped": "Agrupar por liña", diff --git a/src/frontend/app/maps/styleloader.ts b/src/frontend/app/maps/styleloader.ts index cf285a5..08086f1 100644 --- a/src/frontend/app/maps/styleloader.ts +++ b/src/frontend/app/maps/styleloader.ts @@ -1,9 +1,22 @@ import type { StyleSpecification } from "react-map-gl/maplibre"; +import type { Theme } from "~/AppContext"; export async function loadStyle( styleName: string, - colorScheme: string, + colorScheme: Theme, ): Promise<StyleSpecification> { + if (styleName == "openfreemap") { + const url = "/maps/styles/openfreemap-any.json"; + + const resp = await fetch(url); + if (!resp.ok) { + throw new Error(`Failed to load style: ${url}`); + } + + const style = await resp.json(); + return style as StyleSpecification; + } + const stylePath = `/maps/styles/${styleName}-${colorScheme}.json`; const resp = await fetch(stylePath); diff --git a/src/frontend/app/root.css b/src/frontend/app/root.css index b77f44d..202e6f1 100644 --- a/src/frontend/app/root.css +++ b/src/frontend/app/root.css @@ -10,6 +10,7 @@ --star-color: #ffcc00; --message-background-color: #f8f9fa; + color-scheme: light; font-family: "Roboto Variable", Roboto, Arial, sans-serif; } @@ -24,6 +25,8 @@ --button-disabled-background-color: #555555; --star-color: #ffcc00; --message-background-color: #333333; + + color-scheme: dark; } body { diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx index 5887b9c..56a9c79 100644 --- a/src/frontend/app/routes/map.tsx +++ b/src/frontend/app/routes/map.tsx @@ -73,7 +73,8 @@ export default function StopMap() { }, []); useEffect(() => { - const styleName = "carto"; + //const styleName = "carto"; + const styleName = "openfreemap"; loadStyle(styleName, theme) .then((style) => setMapStyle(style)) .catch((error) => console.error("Failed to load map style:", error)); @@ -158,12 +159,33 @@ export default function StopMap() { source="stops-source" layout={{ "icon-image": "stop", - "icon-size": ["interpolate", ["linear"], ["zoom"], 11, 0.4, 16, 0.8], + "icon-size": ["interpolate", ["linear"], ["zoom"], 11, 0.4, 18, 0.8], "icon-allow-overlap": true, "icon-ignore-placement": true, }} /> + <Layer + id="stops-label" + type="symbol" + source="stops-source" + minzoom={16} + layout={{ + "text-field": ["get", "name"], + "text-font": ["Noto Sans Regular"], + "text-offset": [0, 2.5], + "text-anchor": "center", + "text-justify": "center", + "text-size": ["interpolate", ["linear"], ["zoom"], 11, 8, 22, 14] + }} + paint={{ + "text-color": "#45a15a", + "text-halo-color": "#fff", + "text-halo-width": 1.5 + }} + /> + + {selectedStop && ( <StopSheet isOpen={isSheetOpen} diff --git a/src/frontend/app/routes/settings.css b/src/frontend/app/routes/settings.css index 47de391..ef9fbd5 100644 --- a/src/frontend/app/routes/settings.css +++ b/src/frontend/app/routes/settings.css @@ -51,7 +51,6 @@ margin-left: 0.5em; padding: 0.5rem; font-size: 1rem; - border: 1px solid var(--border-color); border-radius: 8px; } diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx index 3bc3492..bcda311 100644 --- a/src/frontend/app/routes/settings.tsx +++ b/src/frontend/app/routes/settings.tsx @@ -1,4 +1,4 @@ -import { useApp } from "../AppContext"; +import { type Theme, useApp } from "../AppContext"; import "./settings.css"; import { useTranslation } from "react-i18next"; import { useState } from "react"; @@ -14,9 +14,6 @@ export default function Settings() { setMapPositionMode, } = useApp(); - const [isCheckingUpdates, setIsCheckingUpdates] = useState(false); - const [updateMessage, setUpdateMessage] = useState<string | null>(null); - return ( <div className="page-container"> <h1 className="page-title">{t("about.title")}</h1> @@ -31,10 +28,11 @@ export default function Settings() { id="theme" className="form-select-inline" value={theme} - onChange={(e) => setTheme(e.target.value as "light" | "dark")} + onChange={(e) => setTheme(e.target.value as Theme)} > <option value="light">{t("about.theme_light")}</option> <option value="dark">{t("about.theme_dark")}</option> + <option value="system">{t("about.theme_system")}</option> </select> </div> <div className="settings-content-inline"> |
