feat: support query theme bootstrap for embeds

This commit is contained in:
2026-04-02 20:41:18 +08:00
parent 83071a4b76
commit 6e55bc02b7
4 changed files with 87 additions and 9 deletions

View File

@@ -1,7 +1,12 @@
"use client";
import { createContext, useContext, useState, useEffect, startTransition, type ReactNode } from "react";
import { resolveIncomingThemeMode, resolveStoredTheme, type ResolvedTheme } from "./theme-sync";
import {
parseThemeQuery,
resolveIncomingThemeMode,
resolveStoredTheme,
type ResolvedTheme,
} from "./theme-sync";
export type Theme = "light" | "dark" | "system";
@@ -51,6 +56,14 @@ function getParentResolvedTheme(): ResolvedTheme | null {
}
}
function getThemeQuery(): Theme | null {
if (typeof window === "undefined") {
return null;
}
return parseThemeQuery(new URLSearchParams(window.location.search).get("theme"));
}
const ThemeContext = createContext<ThemeContextType>({
theme: "system",
setTheme: () => {},
@@ -61,9 +74,18 @@ const ThemeContext = createContext<ThemeContextType>({
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>("system");
const [isEmbedded, setIsEmbedded] = useState(getIsEmbedded);
const [queryTheme, setQueryTheme] = useState<Theme | null>(getThemeQuery);
const [parentResolved, setParentResolved] = useState<ResolvedTheme | null>(getParentResolvedTheme);
const [resolved, setResolved] = useState<ResolvedTheme>(
() => getParentResolvedTheme() ?? resolveStoredTheme("system", getPrefersDark()),
() => {
const nextQueryTheme = getThemeQuery();
return (
getParentResolvedTheme()
?? (nextQueryTheme ? resolveStoredTheme(nextQueryTheme, getPrefersDark()) : null)
?? resolveStoredTheme("system", getPrefersDark())
);
},
);
useEffect(() => {
@@ -77,10 +99,12 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
useEffect(() => {
const nextIsEmbedded = getIsEmbedded();
const nextQueryTheme = getThemeQuery();
const nextParentResolved = getParentResolvedTheme();
startTransition(() => {
setIsEmbedded(nextIsEmbedded);
setQueryTheme(nextQueryTheme);
setParentResolved((current) => current ?? nextParentResolved);
});
}, []);
@@ -111,6 +135,10 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
useEffect(() => {
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const root = document.documentElement;
const activeTheme = queryTheme ?? theme;
const queryResolved = queryTheme
? resolveStoredTheme(queryTheme, mq.matches)
: null;
const applyResolved = (nextResolved: ResolvedTheme) => {
setResolved(nextResolved);
@@ -119,19 +147,23 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
root.setAttribute("data-theme", nextResolved);
};
applyResolved(parentResolved ?? resolveStoredTheme(theme, mq.matches));
applyResolved(
parentResolved
?? queryResolved
?? resolveStoredTheme(theme, mq.matches),
);
if (parentResolved !== null || theme !== "system") {
if (parentResolved !== null || activeTheme !== "system") {
return;
}
const handleSystemThemeChange = (event: MediaQueryListEvent) => {
applyResolved(resolveStoredTheme(theme, event.matches));
applyResolved(resolveStoredTheme(activeTheme, event.matches));
};
mq.addEventListener("change", handleSystemThemeChange);
return () => mq.removeEventListener("change", handleSystemThemeChange);
}, [parentResolved, theme]);
}, [parentResolved, queryTheme, theme]);
const setTheme = (t: Theme) => {
setThemeState(t);