feat: remove dark mode toggle, switch to prefers-color-scheme
This means it's not possible to manually change the theme, but all major operating systems support global dark mode now, so it shouldn't be a huge problem. Will re-add the dark mode toggle if the Sec-CH-Prefers-Color-Scheme header gets added to Firefox and Safari.
This commit is contained in:
		
							parent
							
								
									862a64840e
								
							
						
					
					
						commit
						0f3ab19f6f
					
				
					 9 changed files with 25 additions and 64 deletions
				
			
		|  | @ -1,20 +1,7 @@ | ||||||
| $font-family-sans-serif: | @use "bootstrap/scss/bootstrap" with ( | ||||||
| 	"FiraGO", | 	$color-mode-type: media-query, | ||||||
| 	system-ui, | 	$font-family-sans-serif: ("FiraGO", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"), | ||||||
| 	-apple-system, | ); | ||||||
| 	"Segoe UI", |  | ||||||
| 	Roboto, |  | ||||||
| 	"Helvetica Neue", |  | ||||||
| 	"Noto Sans", |  | ||||||
| 	"Liberation Sans", |  | ||||||
| 	Arial, |  | ||||||
| 	sans-serif, |  | ||||||
| 	"Apple Color Emoji", |  | ||||||
| 	"Segoe UI Emoji", |  | ||||||
| 	"Segoe UI Symbol", |  | ||||||
| 	"Noto Color Emoji" !default; |  | ||||||
| 
 |  | ||||||
| @import "bootstrap/scss/bootstrap"; |  | ||||||
| 
 | 
 | ||||||
| @import "@fontsource/firago/400.css"; | @import "@fontsource/firago/400.css"; | ||||||
| @import "@fontsource/firago/400-italic.css"; | @import "@fontsource/firago/400-italic.css"; | ||||||
|  |  | ||||||
|  | @ -3,9 +3,9 @@ import { Nav, Navbar } from "react-bootstrap"; | ||||||
| import { Link } from "@remix-run/react"; | import { Link } from "@remix-run/react"; | ||||||
| import Logo from "~/components/nav/Logo"; | import Logo from "~/components/nav/Logo"; | ||||||
| 
 | 
 | ||||||
| export default function BaseNavbar({ children, theme }: { children?: ReactNode; theme: string }) { | export default function BaseNavbar({ children }: { children?: ReactNode; }) { | ||||||
| 	return ( | 	return ( | ||||||
| 		<Navbar expand="lg" className={`mb-4 mx-2 bg-${theme}`} color={theme} variant={theme}> | 		<Navbar expand="lg" className={`mb-4 mx-2`}> | ||||||
| 			<Navbar.Brand to="/" as={Link}> | 			<Navbar.Brand to="/" as={Link}> | ||||||
| 				<Logo /> | 				<Logo /> | ||||||
| 			</Navbar.Brand> | 			</Navbar.Brand> | ||||||
|  |  | ||||||
|  | @ -1,19 +1,16 @@ | ||||||
| import { Link, useFetcher } from "@remix-run/react"; | import { Link, useFetcher } from "@remix-run/react"; | ||||||
| import Meta from "~/lib/api/meta"; | import Meta from "~/lib/api/meta"; | ||||||
| import { User, UserSettings } from "~/lib/api/user"; | import { User } from "~/lib/api/user"; | ||||||
| 
 | 
 | ||||||
| import { Nav, NavDropdown } from "react-bootstrap"; | import { Nav, NavDropdown } from "react-bootstrap"; | ||||||
| import { BrightnessHigh, BrightnessHighFill, MoonFill } from "react-bootstrap-icons"; |  | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| import BaseNavbar from "~/components/nav/BaseNavbar"; | import BaseNavbar from "~/components/nav/BaseNavbar"; | ||||||
| 
 | 
 | ||||||
| export default function MainNavbar({ | export default function MainNavbar({ | ||||||
| 	user, | 	user, | ||||||
| 	settings, |  | ||||||
| }: { | }: { | ||||||
| 	meta: Meta; | 	meta: Meta; | ||||||
| 	user?: User; | 	user?: User; | ||||||
| 	settings: UserSettings; |  | ||||||
| }) { | }) { | ||||||
| 	const fetcher = useFetcher(); | 	const fetcher = useFetcher(); | ||||||
| 	const { t } = useTranslation(); | 	const { t } = useTranslation(); | ||||||
|  | @ -38,39 +35,10 @@ export default function MainNavbar({ | ||||||
| 			{t("navbar.log-in")} | 			{t("navbar.log-in")} | ||||||
| 		</Nav.Link> | 		</Nav.Link> | ||||||
| 	); | 	); | ||||||
| 
 | 	 | ||||||
| 	const ThemeIcon = |  | ||||||
| 		settings.dark_mode === null |  | ||||||
| 			? BrightnessHigh |  | ||||||
| 			: settings.dark_mode |  | ||||||
| 				? MoonFill |  | ||||||
| 				: BrightnessHighFill; |  | ||||||
| 
 |  | ||||||
| 	const theme = settings.dark_mode ? "dark" : "light"; |  | ||||||
| 
 |  | ||||||
| 	return ( | 	return ( | ||||||
| 		<BaseNavbar theme={theme}> | 		<BaseNavbar> | ||||||
| 			{userMenu} | 			{userMenu} | ||||||
| 			<fetcher.Form method="POST" action="/dark-mode"> |  | ||||||
| 				<NavDropdown |  | ||||||
| 					title={ |  | ||||||
| 						<> |  | ||||||
| 							<ThemeIcon /> {t("navbar.theme")} |  | ||||||
| 						</> |  | ||||||
| 					} |  | ||||||
| 					align="end" |  | ||||||
| 				> |  | ||||||
| 					<NavDropdown.Item as="button" name="theme" value="auto" type="submit"> |  | ||||||
| 						{t("navbar.theme-auto")} |  | ||||||
| 					</NavDropdown.Item> |  | ||||||
| 					<NavDropdown.Item as="button" name="theme" value="dark" type="submit"> |  | ||||||
| 						{t("navbar.theme-dark")} |  | ||||||
| 					</NavDropdown.Item> |  | ||||||
| 					<NavDropdown.Item as="button" name="theme" value="light" type="submit"> |  | ||||||
| 						{t("navbar.theme-light")} |  | ||||||
| 					</NavDropdown.Item> |  | ||||||
| 				</NavDropdown> |  | ||||||
| 			</fetcher.Form> |  | ||||||
| 		</BaseNavbar> | 		</BaseNavbar> | ||||||
| 	); | 	); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -57,7 +57,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export function Layout({ children }: { children: ReactNode }) { | export function Layout({ children }: { children: ReactNode }) { | ||||||
| 	const { locale, settings } = useRouteLoaderData<typeof loader>("root") || { | 	const { locale } = useRouteLoaderData<typeof loader>("root") || { | ||||||
| 		meta: { | 		meta: { | ||||||
| 			users: { | 			users: { | ||||||
| 				total: 0, | 				total: 0, | ||||||
|  | @ -77,7 +77,6 @@ export function Layout({ children }: { children: ReactNode }) { | ||||||
| 	return ( | 	return ( | ||||||
| 		<html | 		<html | ||||||
| 			lang={locale || "en"} | 			lang={locale || "en"} | ||||||
| 			data-bs-theme={settings?.dark_mode ? "dark" : "light"} |  | ||||||
| 			dir={i18n.dir()} | 			dir={i18n.dir()} | ||||||
| 		> | 		> | ||||||
| 			<head> | 			<head> | ||||||
|  | @ -119,10 +118,10 @@ export function ErrorBoundary() { | ||||||
| 				<Links /> | 				<Links /> | ||||||
| 			</head> | 			</head> | ||||||
| 			<body> | 			<body> | ||||||
| 				{data?.meUser && data?.settings && data?.meta ? ( | 				{data?.meUser && data?.meta ? ( | ||||||
| 					<Navbar meta={data.meta} user={data.meUser} settings={data.settings} /> | 					<Navbar meta={data.meta} user={data.meUser} /> | ||||||
| 				) : ( | 				) : ( | ||||||
| 					<BaseNavbar theme="light" /> | 					<BaseNavbar /> | ||||||
| 				)} | 				)} | ||||||
| 				<Container>{errorElem}</Container> | 				<Container>{errorElem}</Container> | ||||||
| 				<Scripts /> | 				<Scripts /> | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ import PronounLink from "~/components/PronounLink"; | ||||||
| export const meta: MetaFunction<typeof loader> = ({ data }) => { | export const meta: MetaFunction<typeof loader> = ({ data }) => { | ||||||
| 	const { user } = data!; | 	const { user } = data!; | ||||||
| 
 | 
 | ||||||
| 	return [{ title: `@${user.username} - pronouns.cc` }]; | 	return [{ title: `@${user.username} • pronouns.cc` }]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const loader = async ({ request, params }: LoaderFunctionArgs) => { | export const loader = async ({ request, params }: LoaderFunctionArgs) => { | ||||||
|  |  | ||||||
|  | @ -21,7 +21,7 @@ import ErrorAlert from "~/components/ErrorAlert"; | ||||||
| import i18n from "~/i18next.server"; | import i18n from "~/i18next.server"; | ||||||
| 
 | 
 | ||||||
| export const meta: MetaFunction<typeof loader> = ({ data }) => { | export const meta: MetaFunction<typeof loader> = ({ data }) => { | ||||||
| 	return [{ title: `${data?.meta.title || "Log in"} - pronouns.cc` }]; | 	return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { | export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { | ||||||
|  |  | ||||||
|  | @ -21,7 +21,7 @@ import ErrorAlert from "~/components/ErrorAlert"; | ||||||
| import { User } from "~/lib/api/user"; | import { User } from "~/lib/api/user"; | ||||||
| 
 | 
 | ||||||
| export const meta: MetaFunction<typeof loader> = ({ data }) => { | export const meta: MetaFunction<typeof loader> = ({ data }) => { | ||||||
| 	return [{ title: `${data?.meta.title || "Log in"} - pronouns.cc` }]; | 	return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { | export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ import { Link, useLoaderData } from "@remix-run/react"; | ||||||
| import { Button } from "react-bootstrap"; | import { Button } from "react-bootstrap"; | ||||||
| 
 | 
 | ||||||
| export const meta: MetaFunction<typeof loader> = ({ data }) => { | export const meta: MetaFunction<typeof loader> = ({ data }) => { | ||||||
| 	return [{ title: `${data?.meta.title || "Welcome"} - pronouns.cc` }]; | 	return [{ title: `${data?.meta.title || "Welcome"} • pronouns.cc` }]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const loader = async ({ request }: LoaderFunctionArgs) => { | export const loader = async ({ request }: LoaderFunctionArgs) => { | ||||||
|  |  | ||||||
|  | @ -4,6 +4,13 @@ import serverRequest, { getCookie, writeCookie } from "~/lib/request.server"; | ||||||
| 
 | 
 | ||||||
| // Handles theme switching
 | // Handles theme switching
 | ||||||
| // Remix itself handles redirecting back to the original page after the setting is set
 | // Remix itself handles redirecting back to the original page after the setting is set
 | ||||||
|  | //
 | ||||||
|  | // Note: this function is currently unused. Bootstrap only lets us switch themes with either prefers-color-scheme
 | ||||||
|  | // *or* a programmatic switch using data-bs-theme, not both.
 | ||||||
|  | // If the Sec-CH-Prefers-Color-Scheme header (https://caniuse.com/mdn-http_headers_sec-ch-prefers-color-scheme)
 | ||||||
|  | // is added to Firefox and Safari, the dark mode setting should be reworked to use it instead.
 | ||||||
|  | // As it stands, using prefers-color-scheme is the only way
 | ||||||
|  | // to respect the operating system's dark mode setting without using JavaScript.
 | ||||||
| export const action: ActionFunction = async ({ request }) => { | export const action: ActionFunction = async ({ request }) => { | ||||||
| 	const body = await request.formData(); | 	const body = await request.formData(); | ||||||
| 	const theme = (body.get("theme") as string | null) || "auto"; | 	const theme = (body.get("theme") as string | null) || "auto"; | ||||||
|  | @ -13,7 +20,7 @@ export const action: ActionFunction = async ({ request }) => { | ||||||
| 		await serverRequest<UserSettings>("PATCH", "/users/@me/settings", { | 		await serverRequest<UserSettings>("PATCH", "/users/@me/settings", { | ||||||
| 			token, | 			token, | ||||||
| 			body: { | 			body: { | ||||||
| 				dark_mode: theme === "auto" ? null : theme === "dark" ? true : false, | 				dark_mode: theme === "auto" ? null : theme === "dark", | ||||||
| 			}, | 			}, | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue