-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
028bbf2
commit 4004f14
Showing
34 changed files
with
338 additions
and
173 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./useAtomEffect"; |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./hooks"; | ||
export * from "./utils"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
import { noop } from "@fern-ui/core-utils"; | ||
import { ColorsConfig } from "@fern-ui/fdr-utils"; | ||
import { atom, useAtom, useAtomValue } from "jotai"; | ||
import { atomWithRefresh } from "jotai/utils"; | ||
import { createElement, memo } from "react"; | ||
import { useCallbackOne } from "use-memo-one"; | ||
import { z } from "zod"; | ||
import { getThemeColor } from "../next-app/utils/getColorVariables"; | ||
import { useAtomEffect } from "./hooks/useAtomEffect"; | ||
import { atomWithStorageString } from "./utils/atomWithStorageString"; | ||
|
||
const STORAGE_KEY = "theme"; | ||
const SYSTEM = "system" as const; | ||
const SYSTEM_THEMES = ["light" as const, "dark" as const]; | ||
const MEDIA = "(prefers-color-scheme: dark)"; | ||
|
||
type Theme = (typeof SYSTEM_THEMES)[number]; | ||
|
||
const SETTABLE_THEME_ATOM = atomWithStorageString<Theme | typeof SYSTEM>(STORAGE_KEY, SYSTEM, { | ||
validate: z.union([z.literal("system"), z.literal("light"), z.literal("dark")]), | ||
getOnInit: true, | ||
}); | ||
|
||
const IS_SYSTEM_THEME_ATOM = atom((get) => get(SETTABLE_THEME_ATOM) === SYSTEM); | ||
|
||
export const COLORS_ATOM = atom<ColorsConfig>({ dark: undefined, light: undefined }); | ||
export const AVAILABLE_THEMES_ATOM = atom((get) => getAvailableThemes(get(COLORS_ATOM))); | ||
|
||
export function useColors(): ColorsConfig { | ||
return useAtomValue(COLORS_ATOM); | ||
} | ||
|
||
export const THEME_SWITCH_ENABLED_ATOM = atom((get) => { | ||
const availableThemes = get(AVAILABLE_THEMES_ATOM); | ||
return availableThemes.length > 1; | ||
}); | ||
|
||
export const THEME_ATOM = atomWithRefresh((get): Theme => { | ||
const storedTheme = get(SETTABLE_THEME_ATOM); | ||
const availableThemes = get(AVAILABLE_THEMES_ATOM); | ||
if (storedTheme === SYSTEM) { | ||
if (typeof window !== "undefined") { | ||
return getSystemTheme(); | ||
} | ||
} else if (availableThemes.includes(storedTheme)) { | ||
return storedTheme; | ||
} | ||
return availableThemes[0]; | ||
}); | ||
|
||
export function useTheme(): Theme { | ||
return useAtomValue(THEME_ATOM); | ||
} | ||
|
||
export function useSetTheme(): (theme: Theme | typeof SYSTEM) => void { | ||
return useAtom(SETTABLE_THEME_ATOM)[1]; | ||
} | ||
|
||
export function useToggleTheme(): () => void { | ||
const setTheme = useSetTheme(); | ||
const theme = useTheme(); | ||
return () => setTheme(theme === "dark" ? "light" : "dark"); | ||
} | ||
|
||
export const THEME_BG_COLOR = atom((get) => { | ||
const theme = get(THEME_ATOM); | ||
const colors = get(COLORS_ATOM); | ||
const config = colors[theme]; | ||
if (config == null) { | ||
return undefined; | ||
} | ||
return getThemeColor(config); | ||
}); | ||
|
||
const disableAnimation = () => { | ||
if (typeof document === "undefined") { | ||
return noop; | ||
} | ||
|
||
const css = document.createElement("style"); | ||
css.appendChild( | ||
document.createTextNode( | ||
"*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}", | ||
), | ||
); | ||
document.head.appendChild(css); | ||
|
||
return () => { | ||
// Force restyle | ||
(() => window.getComputedStyle(document.body))(); | ||
|
||
// Wait for next tick before removing | ||
setTimeout(() => { | ||
document.head.removeChild(css); | ||
}, 1); | ||
}; | ||
}; | ||
|
||
export type AvailableThemes = [Theme] | [Theme, Theme]; | ||
const getAvailableThemes = (colors: Partial<ColorsConfig> = {}): AvailableThemes => { | ||
if ((colors.dark != null && colors.light != null) || (colors.dark == null && colors.light == null)) { | ||
return ["light", "dark"]; | ||
} | ||
return colors.dark != null ? ["dark"] : ["light"]; | ||
}; | ||
|
||
const getSystemTheme = (e?: MediaQueryList | MediaQueryListEvent) => { | ||
if (!e) { | ||
e = window.matchMedia(MEDIA); | ||
} | ||
const isDark = e.matches; | ||
const systemTheme = isDark ? "dark" : "light"; | ||
return systemTheme; | ||
}; | ||
|
||
export function useInitializeTheme(): void { | ||
useAtomEffect( | ||
useCallbackOne((get) => { | ||
const enableAnimation = disableAnimation(); | ||
const theme = get(THEME_ATOM); | ||
const d = document.documentElement; | ||
d.classList.remove(...SYSTEM_THEMES); | ||
d.classList.add(theme); | ||
d.style.colorScheme = theme; | ||
enableAnimation(); | ||
}, []), | ||
); | ||
|
||
useAtomEffect( | ||
useCallbackOne((get, set) => { | ||
const handleMediaQuery = () => { | ||
if (get(IS_SYSTEM_THEME_ATOM)) { | ||
set(THEME_ATOM); | ||
} | ||
}; | ||
|
||
const media = window.matchMedia(MEDIA); | ||
|
||
// Intentionally use deprecated listener methods to support iOS & old browsers | ||
// eslint-disable-next-line deprecation/deprecation | ||
media.addListener(handleMediaQuery); | ||
handleMediaQuery(); | ||
|
||
// eslint-disable-next-line deprecation/deprecation | ||
return () => media.removeListener(handleMediaQuery); | ||
}, []), | ||
); | ||
} | ||
|
||
// this script cannot reference any other code since it will be stringified to be executed in the browser | ||
export const script = (themes: AvailableThemes): void => { | ||
const el = document.documentElement; | ||
|
||
function updateDOM(theme: string) { | ||
el.classList.remove("light", "dark"); | ||
el.classList.add(theme); | ||
el.style.colorScheme = theme; | ||
} | ||
|
||
function getSystemTheme() { | ||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; | ||
} | ||
|
||
if (themes.length === 1) { | ||
updateDOM(themes[0]); | ||
} else { | ||
try { | ||
const themeName = localStorage.getItem("theme") ?? themes[0]; | ||
const isSystem = themes.length > 0 && themeName === "system"; | ||
const theme = isSystem ? getSystemTheme() : themeName; | ||
updateDOM(theme); | ||
} catch { | ||
// | ||
} | ||
} | ||
}; | ||
|
||
// including the ThemeScript component will prevent a flash of unstyled content (FOUC) when the page loads | ||
// by setting the theme from local storage before the page is rendered | ||
export const ThemeScript = memo( | ||
({ nonce, colors }: { nonce?: string; colors?: ColorsConfig }) => { | ||
const scriptArgs = JSON.stringify(getAvailableThemes(colors)); | ||
|
||
return createElement("script", { | ||
suppressHydrationWarning: true, | ||
nonce: typeof window === "undefined" ? nonce : "", | ||
dangerouslySetInnerHTML: { __html: `(${script.toString()})(${scriptArgs})` }, | ||
}); | ||
}, | ||
(prev, next) => | ||
prev.nonce === next.nonce && getAvailableThemes(prev.colors).length === getAvailableThemes(next.colors).length, | ||
); | ||
|
||
ThemeScript.displayName = "ThemeScript"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { noop } from "@fern-ui/core-utils"; | ||
import { atomWithStorage } from "jotai/utils"; | ||
import { z } from "zod"; | ||
|
||
export function atomWithStorageString<VALUE extends string>( | ||
key: string, | ||
value: VALUE, | ||
{ validate, getOnInit }: { validate?: z.ZodType<VALUE>; getOnInit?: boolean } = {}, | ||
): ReturnType<typeof atomWithStorage<VALUE>> { | ||
return atomWithStorage<VALUE>( | ||
key, | ||
value, | ||
{ | ||
getItem: (key, initialValue) => { | ||
if (typeof window === "undefined") { | ||
return initialValue; | ||
} | ||
|
||
try { | ||
const stored: string | null = window.localStorage.getItem(key); | ||
if (stored == null) { | ||
return initialValue; | ||
} | ||
if (validate) { | ||
const parsed = validate.safeParse(stored); | ||
if (parsed.success) { | ||
return parsed.data; | ||
} | ||
} | ||
} catch { | ||
// ignore | ||
} | ||
return initialValue; | ||
}, | ||
setItem: (key, newValue) => { | ||
if (typeof window === "undefined") { | ||
return; | ||
} | ||
|
||
try { | ||
window.localStorage.setItem(key, newValue); | ||
} catch { | ||
// ignore | ||
} | ||
}, | ||
removeItem: (key) => { | ||
if (typeof window === "undefined") { | ||
return; | ||
} | ||
|
||
try { | ||
window.localStorage.removeItem(key); | ||
} catch { | ||
// ignore | ||
} | ||
}, | ||
subscribe: (key, callback, initialValue) => { | ||
if (typeof window === "undefined") { | ||
return noop; | ||
} | ||
|
||
const listener = (e: StorageEvent) => { | ||
if (e.key === key && e.newValue !== e.oldValue) { | ||
callback( | ||
(validate != null ? validate.safeParse(e.newValue)?.data : (e.newValue as VALUE)) ?? | ||
initialValue, | ||
); | ||
} | ||
}; | ||
window.addEventListener("storage", listener); | ||
return () => { | ||
window.removeEventListener("storage", listener); | ||
}; | ||
}, | ||
}, | ||
{ getOnInit }, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./atomWithStorageString"; |
Oops, something went wrong.