diff --git a/src/ColorProvider/index.tsx b/src/ColorProvider/index.tsx
index 36e57a0..1e8aebc 100644
--- a/src/ColorProvider/index.tsx
+++ b/src/ColorProvider/index.tsx
@@ -1,168 +1,144 @@
import React, {
- useEffect,
- useMemo,
- useState,
- type PropsWithChildren,
+ type PropsWithChildren,
+ useEffect,
+ useInsertionEffect,
+ useLayoutEffect,
+ useMemo,
+ useState,
} from "react";
import ColorContext, {
- type ColorContextValue,
- type ColorScheme,
- type ColorSchemePreference,
+ type ColorContextValue,
+ type ColorScheme,
+ type ColorSchemePreference,
} from "../ColorContext";
import useSystemColorScheme from "../hooks/useSystemColorScheme";
import { applyTheme } from "../theme";
-// License notes: a lot of the code having to do with runtime reactive switching came from GitHub's MIT code:
-// https://github.com/primer/react/blob/e1268ff35acf48561adef9e55f8add39f69924eb/packages/react/src/ThemeProvider.tsx#L146
+const STORAGE_KEY =
+ "@determinate-systems/ui/ColorProvider/scheme-preference";
export interface ColorProviderProps {
- /** Root element for this color context. Defaults to the HTML `
` element, but can be scoped more narrowly for testing. */
- root?: Element;
+ /** Root element for this color context. Defaults to the HTML `` element, but can be scoped more narrowly for testing. */
+ root?: Element | undefined;
- /**
- * Sync the user's preferred color scheme to local storage.
- *
- * Defaults to enabled, specify false to turn it off.
- **/
- useLocalStorage?: boolean;
+ /**
+ * Sync the user's preferred color scheme to local storage.
+ *
+ * Defaults to enabled, specify false to turn it off.
+ **/
+ useLocalStorage?: boolean;
- /** (For testing) Which color scheme to use instead of querying the system? */
- simulatedSystemColorScheme?: ColorScheme;
+ /** (For testing) Which color scheme to use instead of querying the system? */
+ simulatedSystemColorScheme?: ColorScheme;
- /** (For testing) Which color scheme does the user prefer? */
- preferredColorScheme?: ColorSchemePreference;
+ /** (For testing) Which color scheme does the user prefer? */
+ preferredColorScheme?: ColorSchemePreference;
}
function computeInitialColorSchemePreference(
- useLocalStorage: boolean,
- preferredColorScheme?: ColorSchemePreference,
+ useLocalStorage: boolean,
+ preferredColorScheme?: ColorSchemePreference,
): ColorSchemePreference {
- if (useLocalStorage) {
- const storedPreference = readSchemeFromLocalStorage();
- if (storedPreference) {
- return storedPreference;
- }
- }
-
- if (preferredColorScheme) {
- return preferredColorScheme;
- }
-
- // In case we're doing server-side rendering, just render `dark` be done with it.
- if (typeof window === "undefined") {
- return "dark";
- }
-
- return "auto";
+ if (useLocalStorage) {
+ const storedPreference = readSchemeFromLocalStorage();
+ if (storedPreference) return storedPreference;
+ }
+
+ if (preferredColorScheme) return preferredColorScheme;
+
+ // SPA default
+ return "auto";
}
function readSchemeFromLocalStorage(): ColorSchemePreference | undefined {
- let pref;
-
- try {
- pref = localStorage.getItem(
- "@determinate-systems/ui/ColorProvider/scheme-preference",
- );
- } catch {
- return undefined;
- }
-
- switch (pref) {
- case "auto":
- case "light":
- case "dark":
- return pref;
- default:
- return undefined;
- }
+ const value = localStorage.getItem(STORAGE_KEY);
+ return value === "auto" || value === "light" || value === "dark"
+ ? value
+ : undefined;
}
function writeSchemeToLocalStorage(preference: ColorSchemePreference) {
- try {
- localStorage.setItem(
- "@determinate-systems/ui/ColorProvider/scheme-preference",
- preference,
- );
- } catch {
- // Ignore errors
- }
+ try {
+ localStorage.setItem(
+ "@determinate-systems/ui/ColorProvider/scheme-preference",
+ preference,
+ );
+ } catch {
+ // Ignore errors
+ }
}
-// Helper component for
-const ColorProvider: React.FC> = ({
- useLocalStorage = true,
- simulatedSystemColorScheme,
- preferredColorScheme,
- root = document.body,
- children,
-}) => {
- const actualSystemColorScheme = useSystemColorScheme();
- const systemColorScheme =
- simulatedSystemColorScheme ?? actualSystemColorScheme;
-
- const [preference, setPreference] = useState(() =>
- computeInitialColorSchemePreference(useLocalStorage, preferredColorScheme),
- );
- const [scheme, setScheme] = useState(() =>
- resolveColorScheme(preference, systemColorScheme),
- );
-
- // Apply the theme super early so we don't get a FOUC
- applyTheme(root, scheme);
-
- // Since we're pretty high up in the component tree, we want to be extremely
- // careful about re-rendering. Memoization ensures that the object only
- // changes when the scheme or preference does.
- const value = useMemo(
- () => ({ scheme, setScheme, preference, setPreference }),
- [scheme, preference],
- );
-
- // Switch body classes depending on the chosen scheme
- useEffect(() => {
- writeSchemeToLocalStorage(preference);
- setScheme(resolveColorScheme(preference, systemColorScheme));
- }, [preference, systemColorScheme, setScheme]);
-
- // Switch body classes depending on the chosen scheme
- useEffect(() => {
- applyTheme(root, scheme);
- }, [scheme]);
-
- return (
- {children}
- );
-};
-
function resolveColorScheme(
- preferredScheme: ColorSchemePreference,
- systemColorScheme?: ColorScheme,
+ preferredScheme: ColorSchemePreference,
+ systemColorScheme?: ColorScheme,
): ColorScheme {
- switch (preferredScheme) {
- case "auto":
- if (systemColorScheme) {
- return systemColorScheme;
- }
- return "dark";
- default:
- return preferredScheme;
- }
+ if (preferredScheme === "auto") return systemColorScheme ?? "dark";
+ return preferredScheme;
}
-const nextSchemePreference = (
- preference: ColorSchemePreference,
+// Apply as early as React allows (insertion), else layout.
+const useEarlyEffect =
+ typeof useInsertionEffect === "function"
+ ? useInsertionEffect
+ : useLayoutEffect;
+
+const ColorProvider: React.FC> = ({
+ useLocalStorage = true,
+ simulatedSystemColorScheme,
+ preferredColorScheme,
+ root: initialRoot = undefined,
+ children,
+}) => {
+ const actualSystemColorScheme = useSystemColorScheme();
+ const systemColorScheme =
+ simulatedSystemColorScheme ?? actualSystemColorScheme;
+
+ const root: Element | null =
+ initialRoot ?? (typeof document !== "undefined" ? document.body : null);
+
+ const [preference, setPreference] = useState(() =>
+ computeInitialColorSchemePreference(useLocalStorage, preferredColorScheme),
+ );
+
+ const [scheme, setScheme] = useState(() =>
+ resolveColorScheme(preference, systemColorScheme),
+ );
+
+ // Sync scheme whenever preference/system changes
+ useEffect(() => {
+ if (useLocalStorage) writeSchemeToLocalStorage(preference);
+ setScheme(resolveColorScheme(preference, systemColorScheme));
+ }, [preference, systemColorScheme, useLocalStorage]);
+
+ // Apply the theme "super early" (without doing it during render)
+ useEarlyEffect(() => {
+ if (!root) return;
+ applyTheme(root, scheme);
+ }, [root, scheme]);
+
+ const value = useMemo(
+ () => ({ scheme, setScheme, preference, setPreference }),
+ [scheme, preference],
+ );
+
+ return (
+ {children}
+ );
+};
+
+export const nextSchemePreference = (
+ preference: ColorSchemePreference,
): ColorSchemePreference => {
- switch (preference) {
- case "auto":
- return "dark";
- case "dark":
- return "light";
- case "light":
- return "auto";
- }
+ switch (preference) {
+ case "auto":
+ return "dark";
+ case "dark":
+ return "light";
+ case "light":
+ return "auto";
+ }
};
-export { nextSchemePreference };
export default ColorProvider;
diff --git a/src/ColorSchemeToggle/index.tsx b/src/ColorSchemeToggle/index.tsx
index 18e9cb3..3923bed 100644
--- a/src/ColorSchemeToggle/index.tsx
+++ b/src/ColorSchemeToggle/index.tsx
@@ -1,47 +1,44 @@
-import { useContext, type FC } from "react";
+import { type FC, useContext, useMemo } from "react";
+import { IoMoonSharp, IoSunnySharp } from "react-icons/io5";
import ColorContext, { type ColorSchemePreference } from "../ColorContext";
import { nextSchemePreference } from "../ColorProvider";
-import { IoMoonSharp, IoSunnySharp } from "react-icons/io5";
import AutoIcon from "./AutoIcon";
import "./index.scss";
const icons = {
- light: ,
- dark: ,
- auto: ,
+ light: ,
+ dark: ,
+ auto: ,
};
-export interface ColorSchemeToggleProps {}
+const ColorSchemeToggle: FC = () => {
+ const ctx = useContext(ColorContext);
-const ColorSchemeToggle: FC = ({}) => {
- const colorSchemeCtx = useContext(ColorContext);
+ const icon = useMemo(() => icons[ctx.preference], [ctx.preference]);
- return (
-
- );
+ return (
+
+ );
};
export interface IconProps {
- preference: ColorSchemePreference;
+ preference: ColorSchemePreference;
}
-const Icon: FC = ({ preference }) => {
- return icons[preference];
-};
-
export default ColorSchemeToggle;
diff --git a/src/index.ts b/src/index.ts
index 5821ba8..566e50f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,87 +2,65 @@
import "./sass/preflight.scss";
import "./sass/reset.scss";
-export { applyTheme, themeForScheme } from "./theme";
-
-// Hooks
-export {
- type HighlightLanguage,
- default as useHighlight,
-} from "./hooks/useHighlight";
-export { default as useObjectURL } from "./hooks/useObjectURL";
-
// Components
export {
- type BusyIconProps,
- type BusyIconSize,
- default as BusyIcon,
+ type BusyIconProps,
+ type BusyIconSize,
+ default as BusyIcon,
} from "./BusyIcon";
-
export { type ButtonProps, default as Button } from "./Button";
-
export { type CodeBlockProps, default as CodeBlock } from "./CodeBlock";
-
export { type CodeFileProps, default as CodeFile } from "./CodeFile";
-
export { type CodeInlineProps, default as CodeInline } from "./CodeInline";
-
export {
- type ColorContextValue,
- type ColorScheme,
- default as ColorContext,
+ type ColorContextValue,
+ type ColorScheme,
+ default as ColorContext,
} from "./ColorContext";
-
-export {
- type ColorSchemeToggleProps,
- default as ColorSchemeToggle,
-} from "./ColorSchemeToggle";
-
export {
- type ColorProviderProps,
- default as ColorProvider,
+ type ColorProviderProps,
+ default as ColorProvider,
} from "./ColorProvider";
-
+export {
+ default as ColorSchemeToggle,
+} from "./ColorSchemeToggle";
export { type CopyButtonProps, default as CopyButton } from "./CopyButton";
-
export { type DetSysIconProps, default as DetSysIcon } from "./DetSysIcon";
-
export {
- type DownloadButtonProps,
- default as DownloadButton,
+ type DownloadButtonProps,
+ default as DownloadButton,
} from "./DownloadButton";
-export { type HighlightProps, default as Highlight } from "./Highlight";
-
-export {
- type CTAType,
- type InstallCTAProps,
- default as InstallCTA,
-} from "./InstallCTA";
-
-export { type LinkProps, default as Link } from "./Link";
-
-export { default as MacInstaller } from "./MacInstaller";
-
export {
- type GitHubButtonProps,
- default as GitHubButton,
+ default as GitHubButton,
+ type GitHubButtonProps,
} from "./GitHubButton";
-
-export { type HeaderProps, default as Header } from "./Header";
-
+export { default as Header, type HeaderProps } from "./Header";
+export { default as Highlight, type HighlightProps } from "./Highlight";
+// Hooks
export {
- type LabeledInputProps,
- type LabeledInputRenderProps,
- default as LabeledInput,
+ default as useHighlight,
+ type HighlightLanguage,
+} from "./hooks/useHighlight";
+export { default as useObjectURL } from "./hooks/useObjectURL";
+export {
+ type CTAType,
+ default as InstallCTA,
+ type InstallCTAProps,
+} from "./InstallCTA";
+export {
+ default as LabeledInput,
+ type LabeledInputProps,
+ type LabeledInputRenderProps,
} from "./LabeledInput";
-
export {
- type LabeledRadioInputProps,
- default as LabeledRadioInput,
+ default as LabeledRadioInput,
+ type LabeledRadioInputProps,
} from "./LabeledRadioInput";
-
export {
- type LabeledTextInputProps,
- default as LabeledTextInput,
+ default as LabeledTextInput,
+ type LabeledTextInputProps,
} from "./LabeledTextInput";
-
-export { type PageLayoutProps, default as PageLayout } from "./PageLayout";
+export { default as Link, type LinkProps } from "./Link";
+export { default as MacInstaller } from "./MacInstaller";
+export { default as PageLayout, type PageLayoutProps } from "./PageLayout";
+export { applyTheme, themeForScheme } from "./theme";