From 011f7c8a662c73a6505724e4551546822ed64004 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Fri, 23 Jan 2026 17:55:50 -0300 Subject: [PATCH 1/3] Make ColorProvider more friendly to React Router --- src/ColorProvider/index.tsx | 241 +++++++++++++++++------------------- src/index.ts | 101 ++++++--------- 2 files changed, 155 insertions(+), 187 deletions(-) diff --git a/src/ColorProvider/index.tsx b/src/ColorProvider/index.tsx index 36e57a0..21ea8f9 100644 --- a/src/ColorProvider/index.tsx +++ b/src/ColorProvider/index.tsx @@ -1,18 +1,17 @@ -import React, { - useEffect, - useMemo, - useState, - type PropsWithChildren, +import { + type FC, + type PropsWithChildren, + useEffect, + useMemo, + useState, } from "react"; - -import ColorContext, { - type ColorContextValue, - type ColorScheme, - type ColorSchemePreference, +import { applyTheme, ColorContext } from ".."; +import type { + ColorContextValue, + ColorScheme, + 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 @@ -35,134 +34,124 @@ export interface ColorProviderProps { preferredColorScheme?: ColorSchemePreference; } +const ColorProvider: FC> = ({ + useLocalStorage = true, + simulatedSystemColorScheme, + preferredColorScheme, + children, +}) => { + const actualSystemColorScheme = useSystemColorScheme(); + const systemColorScheme = + simulatedSystemColorScheme ?? actualSystemColorScheme; + + const [preference, setPreference] = useState(() => + computeInitialColorSchemePreference(useLocalStorage, preferredColorScheme), + ); + const [scheme, setScheme] = useState(() => + resolveColorScheme(preference, systemColorScheme), + ); + + const value = useMemo( + () => ({ scheme, setScheme, preference, setPreference }), + [scheme, preference], + ); + + useEffect(() => { + writeSchemeToLocalStorage(preference); + setScheme(resolveColorScheme(preference, systemColorScheme)); + }, [preference, systemColorScheme]); + + useEffect(() => { + applyTheme(document.body, scheme); + }, [scheme]); + + return ( + {children} + ); +}; + 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; + } + + // In case we're doing server-side rendering, just render `dark` be done with it. + if (typeof window === "undefined") { + return "dark"; + } + + 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; - } -} - -function writeSchemeToLocalStorage(preference: ColorSchemePreference) { - try { - localStorage.setItem( - "@determinate-systems/ui/ColorProvider/scheme-preference", - preference, - ); - } catch { - // Ignore errors - } + let pref: ColorSchemePreference; + + 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; + } } -// 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; - } + switch (preferredScheme) { + case "auto": + if (systemColorScheme) { + return systemColorScheme; + } + return "dark"; + default: + return preferredScheme; + } } const nextSchemePreference = ( - preference: ColorSchemePreference, + 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"; + } }; +function writeSchemeToLocalStorage(preference: ColorSchemePreference) { + try { + localStorage.setItem( + "@determinate-systems/ui/ColorProvider/scheme-preference", + preference, + ); + } catch { + // Ignore errors + } +} + export { nextSchemePreference }; export default ColorProvider; diff --git a/src/index.ts b/src/index.ts index 5821ba8..ce98db5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,87 +2,66 @@ 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 { + type ColorSchemeToggleProps, + 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"; From 97f39655ff6dfc84eb8eb6445e1f860063ce2118 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Fri, 23 Jan 2026 19:01:38 -0300 Subject: [PATCH 2/3] Fix build issues --- src/ColorProvider/index.tsx | 47 +++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/src/ColorProvider/index.tsx b/src/ColorProvider/index.tsx index 21ea8f9..2c2cde8 100644 --- a/src/ColorProvider/index.tsx +++ b/src/ColorProvider/index.tsx @@ -5,33 +5,34 @@ import { useMemo, useState, } from "react"; -import { applyTheme, ColorContext } from ".."; -import type { - ColorContextValue, - ColorScheme, - ColorSchemePreference, +import ColorContext, { + 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 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; - /** - * 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; } const ColorProvider: FC> = ({ @@ -94,15 +95,9 @@ function computeInitialColorSchemePreference( } function readSchemeFromLocalStorage(): ColorSchemePreference | undefined { - let pref: ColorSchemePreference; - - try { - pref = localStorage.getItem( - "@determinate-systems/ui/ColorProvider/scheme-preference", - ); - } catch { - return undefined; - } + const pref = localStorage.getItem( + "@determinate-systems/ui/ColorProvider/scheme-preference", + ); switch (pref) { case "auto": From eee6670ade4ace1de5618fed6a71ad6ba67383ad Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Fri, 23 Jan 2026 20:05:57 -0300 Subject: [PATCH 3/3] Remove unnecessary try/catch --- src/ColorProvider/index.tsx | 164 +++++++++++++++----------------- src/ColorSchemeToggle/index.tsx | 59 ++++++------ src/index.ts | 1 - 3 files changed, 106 insertions(+), 118 deletions(-) diff --git a/src/ColorProvider/index.tsx b/src/ColorProvider/index.tsx index 2c2cde8..1e8aebc 100644 --- a/src/ColorProvider/index.tsx +++ b/src/ColorProvider/index.tsx @@ -1,25 +1,27 @@ -import { - type FC, +import React, { 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 | undefined; /** * Sync the user's preferred color scheme to local storage. @@ -35,96 +37,98 @@ export interface ColorProviderProps { preferredColorScheme?: ColorSchemePreference; } -const ColorProvider: FC> = ({ +function computeInitialColorSchemePreference( + useLocalStorage: boolean, + preferredColorScheme?: ColorSchemePreference, +): ColorSchemePreference { + if (useLocalStorage) { + const storedPreference = readSchemeFromLocalStorage(); + if (storedPreference) return storedPreference; + } + + if (preferredColorScheme) return preferredColorScheme; + + // SPA default + return "auto"; +} + +function readSchemeFromLocalStorage(): ColorSchemePreference | 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 + } +} + +function resolveColorScheme( + preferredScheme: ColorSchemePreference, + systemColorScheme?: ColorScheme, +): ColorScheme { + if (preferredScheme === "auto") return systemColorScheme ?? "dark"; + return preferredScheme; +} + +// 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 [preference, setPreference] = useState(() => + const root: Element | null = + initialRoot ?? (typeof document !== "undefined" ? document.body : null); + + const [preference, setPreference] = useState(() => computeInitialColorSchemePreference(useLocalStorage, preferredColorScheme), ); - const [scheme, setScheme] = useState(() => + + 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], ); - useEffect(() => { - writeSchemeToLocalStorage(preference); - setScheme(resolveColorScheme(preference, systemColorScheme)); - }, [preference, systemColorScheme]); - - useEffect(() => { - applyTheme(document.body, scheme); - }, [scheme]); - return ( {children} ); }; -function computeInitialColorSchemePreference( - 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"; -} - -function readSchemeFromLocalStorage(): ColorSchemePreference | undefined { - const pref = localStorage.getItem( - "@determinate-systems/ui/ColorProvider/scheme-preference", - ); - - switch (pref) { - case "auto": - case "light": - case "dark": - return pref; - default: - return undefined; - } -} - -function resolveColorScheme( - preferredScheme: ColorSchemePreference, - systemColorScheme?: ColorScheme, -): ColorScheme { - switch (preferredScheme) { - case "auto": - if (systemColorScheme) { - return systemColorScheme; - } - return "dark"; - default: - return preferredScheme; - } -} - -const nextSchemePreference = ( +export const nextSchemePreference = ( preference: ColorSchemePreference, ): ColorSchemePreference => { switch (preference) { @@ -137,16 +141,4 @@ const nextSchemePreference = ( } }; -function writeSchemeToLocalStorage(preference: ColorSchemePreference) { - try { - localStorage.setItem( - "@determinate-systems/ui/ColorProvider/scheme-preference", - preference, - ); - } catch { - // Ignore errors - } -} - -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 ce98db5..566e50f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,6 @@ export { default as ColorProvider, } from "./ColorProvider"; export { - type ColorSchemeToggleProps, default as ColorSchemeToggle, } from "./ColorSchemeToggle"; export { type CopyButtonProps, default as CopyButton } from "./CopyButton";