A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1import { useEffect, useState } from "react";
2
3/**
4 * Possible user-facing color scheme preferences.
5 */
6export type ColorSchemePreference = "light" | "dark" | "system";
7
8const MEDIA_QUERY = "(prefers-color-scheme: dark)";
9
10/**
11 * Resolves a persisted preference into an explicit light/dark value.
12 *
13 * @param pref - Stored preference value (`light`, `dark`, or `system`).
14 * @returns Explicit light/dark scheme suitable for rendering.
15 */
16function resolveScheme(pref: ColorSchemePreference): "light" | "dark" {
17 if (pref === "light" || pref === "dark") return pref;
18 if (
19 typeof window === "undefined" ||
20 typeof window.matchMedia !== "function"
21 ) {
22 return "light";
23 }
24 return window.matchMedia(MEDIA_QUERY).matches ? "dark" : "light";
25}
26
27/**
28 * React hook that returns the effective light/dark scheme, respecting system preferences.
29 *
30 * @param preference - User preference; defaults to following the OS setting.
31 * @returns {'light' | 'dark'} Explicit scheme that should be used for rendering.
32 */
33export function useColorScheme(
34 preference: ColorSchemePreference = "system",
35): "light" | "dark" {
36 const [scheme, setScheme] = useState<"light" | "dark">(() =>
37 resolveScheme(preference),
38 );
39
40 useEffect(() => {
41 if (preference === "light" || preference === "dark") {
42 setScheme(preference);
43 return;
44 }
45 if (
46 typeof window === "undefined" ||
47 typeof window.matchMedia !== "function"
48 ) {
49 setScheme("light");
50 return;
51 }
52 const media = window.matchMedia(MEDIA_QUERY);
53 const update = (event: MediaQueryListEvent | MediaQueryList) => {
54 setScheme(event.matches ? "dark" : "light");
55 };
56 update(media);
57 if (typeof media.addEventListener === "function") {
58 media.addEventListener("change", update);
59 return () => media.removeEventListener("change", update);
60 }
61 media.addListener(update);
62 return () => media.removeListener(update);
63 }, [preference]);
64
65 return scheme;
66}