Formatted, fixed some ttp stuff

This commit is contained in:
Alois 2026-03-14 19:58:13 +01:00
commit d77271e4b7
27 changed files with 1089 additions and 371 deletions

View file

@ -1,79 +1,79 @@
import * as React from "react"
import * as React from "react";
type Theme = "dark" | "light" | "system"
type ResolvedTheme = "dark" | "light"
type Theme = "dark" | "light" | "system";
type ResolvedTheme = "dark" | "light";
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
disableTransitionOnChange?: boolean
}
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
disableTransitionOnChange?: boolean;
};
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
theme: Theme;
setTheme: (theme: Theme) => void;
};
const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)"
const THEME_VALUES: Theme[] = ["dark", "light", "system"]
const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)";
const THEME_VALUES: Theme[] = ["dark", "light", "system"];
const ThemeProviderContext = React.createContext<
ThemeProviderState | undefined
>(undefined)
>(undefined);
function isTheme(value: string | null): value is Theme {
if (value === null) {
return false
return false;
}
return THEME_VALUES.includes(value as Theme)
return THEME_VALUES.includes(value as Theme);
}
function getSystemTheme(): ResolvedTheme {
if (window.matchMedia(COLOR_SCHEME_QUERY).matches) {
return "dark"
return "dark";
}
return "light"
return "light";
}
function disableTransitionsTemporarily() {
const style = document.createElement("style")
const style = document.createElement("style");
style.appendChild(
document.createTextNode(
"*,*::before,*::after{-webkit-transition:none!important;transition:none!important}"
)
)
document.head.appendChild(style)
"*,*::before,*::after{-webkit-transition:none!important;transition:none!important}",
),
);
document.head.appendChild(style);
return () => {
window.getComputedStyle(document.body)
window.getComputedStyle(document.body);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
style.remove()
})
})
}
style.remove();
});
});
};
}
function isEditableTarget(target: EventTarget | null) {
if (!(target instanceof HTMLElement)) {
return false
return false;
}
if (target.isContentEditable) {
return true
return true;
}
const editableParent = target.closest(
"input, textarea, select, [contenteditable='true']"
)
"input, textarea, select, [contenteditable='true']",
);
if (editableParent) {
return true
return true;
}
return false
return false;
}
export function ThemeProvider({
@ -84,76 +84,76 @@ export function ThemeProvider({
...props
}: ThemeProviderProps) {
const [theme, setThemeState] = React.useState<Theme>(() => {
const storedTheme = localStorage.getItem(storageKey)
const storedTheme = localStorage.getItem(storageKey);
if (isTheme(storedTheme)) {
return storedTheme
return storedTheme;
}
return defaultTheme
})
return defaultTheme;
});
const setTheme = React.useCallback(
(nextTheme: Theme) => {
localStorage.setItem(storageKey, nextTheme)
setThemeState(nextTheme)
localStorage.setItem(storageKey, nextTheme);
setThemeState(nextTheme);
},
[storageKey]
)
[storageKey],
);
const applyTheme = React.useCallback(
(nextTheme: Theme) => {
const root = document.documentElement
const root = document.documentElement;
const resolvedTheme =
nextTheme === "system" ? getSystemTheme() : nextTheme
nextTheme === "system" ? getSystemTheme() : nextTheme;
const restoreTransitions = disableTransitionOnChange
? disableTransitionsTemporarily()
: null
: null;
root.classList.remove("light", "dark")
root.classList.add(resolvedTheme)
root.classList.remove("light", "dark");
root.classList.add(resolvedTheme);
if (restoreTransitions) {
restoreTransitions()
restoreTransitions();
}
},
[disableTransitionOnChange]
)
[disableTransitionOnChange],
);
React.useEffect(() => {
applyTheme(theme)
applyTheme(theme);
if (theme !== "system") {
return undefined
return undefined;
}
const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY)
const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY);
const handleChange = () => {
applyTheme("system")
}
applyTheme("system");
};
mediaQuery.addEventListener("change", handleChange)
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange)
}
}, [theme, applyTheme])
mediaQuery.removeEventListener("change", handleChange);
};
}, [theme, applyTheme]);
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.repeat) {
return
return;
}
if (event.metaKey || event.ctrlKey || event.altKey) {
return
return;
}
if (isEditableTarget(event.target)) {
return
return;
}
if (event.key.toLowerCase() !== "d") {
return
return;
}
setThemeState((currentTheme) => {
@ -164,66 +164,66 @@ export function ThemeProvider({
? "dark"
: getSystemTheme() === "dark"
? "light"
: "dark"
: "dark";
localStorage.setItem(storageKey, nextTheme)
return nextTheme
})
}
localStorage.setItem(storageKey, nextTheme);
return nextTheme;
});
};
window.addEventListener("keydown", handleKeyDown)
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown)
}
}, [storageKey])
window.removeEventListener("keydown", handleKeyDown);
};
}, [storageKey]);
React.useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.storageArea !== localStorage) {
return
return;
}
if (event.key !== storageKey) {
return
return;
}
if (isTheme(event.newValue)) {
setThemeState(event.newValue)
return
setThemeState(event.newValue);
return;
}
setThemeState(defaultTheme)
}
setThemeState(defaultTheme);
};
window.addEventListener("storage", handleStorageChange)
window.addEventListener("storage", handleStorageChange);
return () => {
window.removeEventListener("storage", handleStorageChange)
}
}, [defaultTheme, storageKey])
window.removeEventListener("storage", handleStorageChange);
};
}, [defaultTheme, storageKey]);
const value = React.useMemo(
() => ({
theme,
setTheme,
}),
[theme, setTheme]
)
[theme, setTheme],
);
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
);
}
export const useTheme = () => {
const context = React.useContext(ThemeProviderContext)
const context = React.useContext(ThemeProviderContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider")
throw new Error("useTheme must be used within a ThemeProvider");
}
return context
}
return context;
};