remix-themes
Version:
An abstraction for themes in your Remix_run app.
279 lines (270 loc) • 8.22 kB
JavaScript
// src/theme-provider.tsx
import {
createContext,
useCallback as useCallback3,
useContext,
useEffect as useEffect2,
useMemo,
useState
} from "react";
// src/useBroadcastChannel.ts
import { useCallback, useEffect, useRef } from "react";
function useBroadcastChannel(channelName, handleMessage, handleMessageError) {
const channelRef = useRef(
typeof window !== "undefined" && "BroadcastChannel" in window ? new BroadcastChannel(`${channelName}-channel`) : null
);
useChannelEventListener(channelRef, "message", handleMessage);
useChannelEventListener(channelRef, "messageerror", handleMessageError);
return useCallback((data) => {
var _a;
(_a = channelRef == null ? void 0 : channelRef.current) == null ? void 0 : _a.postMessage(data);
}, []);
}
function useChannelEventListener(channelRef, event, handler = () => {
}) {
useEffect(() => {
const channel = channelRef.current;
if (channel) {
channel.addEventListener(event, handler);
return () => channel.removeEventListener(event, handler);
}
}, [event, handler]);
}
// src/useCorrectCssTransition.ts
import { useCallback as useCallback2 } from "react";
function withoutTransition(callback) {
const css = document.createElement("style");
css.appendChild(
document.createTextNode(
`* {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
-ms-transition: none !important;
transition: none !important;
}`
)
);
document.head.appendChild(css);
callback();
setTimeout(() => {
const _ = window.getComputedStyle(css).transition;
document.head.removeChild(css);
}, 100);
}
function useCorrectCssTransition({
disableTransitions = false
} = {}) {
return useCallback2(
(callback) => {
if (disableTransitions) {
withoutTransition(() => {
callback();
});
} else {
callback();
}
},
[disableTransitions]
);
}
// src/theme-provider.tsx
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
var Theme = /* @__PURE__ */ ((Theme2) => {
Theme2["DARK"] = "dark";
Theme2["LIGHT"] = "light";
return Theme2;
})(Theme || {});
var themes = Object.values(Theme);
var ThemeContext = createContext(void 0);
ThemeContext.displayName = "ThemeContext";
var prefersLightMQ = "(prefers-color-scheme: light)";
var getPreferredTheme = () => window.matchMedia(prefersLightMQ).matches ? "light" /* LIGHT */ : "dark" /* DARK */;
var mediaQuery = typeof window !== "undefined" ? window.matchMedia(prefersLightMQ) : null;
function ThemeProvider({
children,
specifiedTheme,
themeAction,
disableTransitionOnThemeChange = false
}) {
const ensureCorrectTransition = useCorrectCssTransition({
disableTransitions: disableTransitionOnThemeChange
});
const [theme, setTheme] = useState(() => {
if (specifiedTheme) {
return themes.includes(specifiedTheme) ? specifiedTheme : null;
}
if (typeof window !== "object") return null;
return getPreferredTheme();
});
const [themeDefinedBy, setThemeDefinedBy] = useState(specifiedTheme ? "USER" : "SYSTEM");
const broadcastThemeChange = useBroadcastChannel("remix-themes", (e) => {
ensureCorrectTransition(() => {
console.log("broadcastThemeChange", disableTransitionOnThemeChange);
setTheme(e.data.theme);
setThemeDefinedBy(e.data.definedBy);
});
});
useEffect2(() => {
if (themeDefinedBy === "USER") {
return () => {
};
}
const handleChange = (ev) => {
ensureCorrectTransition(() => {
setTheme(ev.matches ? "light" /* LIGHT */ : "dark" /* DARK */);
});
};
mediaQuery == null ? void 0 : mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery == null ? void 0 : mediaQuery.removeEventListener("change", handleChange);
}, [ensureCorrectTransition, themeDefinedBy]);
const handleThemeChange = useCallback3(
(value2) => {
const nextTheme = typeof value2 === "function" ? value2(theme) : value2;
if (nextTheme === null) {
const preferredTheme = getPreferredTheme();
ensureCorrectTransition(() => {
setTheme(preferredTheme);
setThemeDefinedBy("SYSTEM");
broadcastThemeChange({ theme: preferredTheme, definedBy: "SYSTEM" });
});
fetch(`${themeAction}`, {
method: "POST",
body: JSON.stringify({ theme: null })
});
} else {
ensureCorrectTransition(() => {
setTheme(nextTheme);
setThemeDefinedBy("USER");
});
broadcastThemeChange({ theme: nextTheme, definedBy: "USER" });
fetch(`${themeAction}`, {
method: "POST",
body: JSON.stringify({ theme: nextTheme })
});
}
},
[broadcastThemeChange, ensureCorrectTransition, theme, themeAction]
);
const value = useMemo(
() => [theme, handleThemeChange, { definedBy: themeDefinedBy }],
[theme, handleThemeChange, themeDefinedBy]
);
return /* @__PURE__ */ jsx(ThemeContext.Provider, { value, children });
}
var clientThemeCode = String.raw`
(() => {
const theme = window.matchMedia(${JSON.stringify(prefersLightMQ)}).matches
? 'light'
: 'dark';
const cl = document.documentElement.classList;
const dataAttr = document.documentElement.dataset.theme;
if (dataAttr != null) {
const themeAlreadyApplied = dataAttr === 'light' || dataAttr === 'dark';
if (!themeAlreadyApplied) {
document.documentElement.dataset.theme = theme;
}
} else {
const themeAlreadyApplied = cl.contains('light') || cl.contains('dark');
if (!themeAlreadyApplied) {
cl.add(theme);
}
}
const meta = document.querySelector('meta[name=color-scheme]');
if (meta) {
if (theme === 'dark') {
meta.content = 'dark light';
} else if (theme === 'light') {
meta.content = 'light dark';
}
}
})();
`;
function PreventFlashOnWrongTheme({
ssrTheme,
nonce
}) {
const [theme] = useTheme();
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(
"meta",
{
name: "color-scheme",
content: theme === "light" ? "light dark" : "dark light"
}
),
ssrTheme ? null : /* @__PURE__ */ jsx(
"script",
{
dangerouslySetInnerHTML: { __html: clientThemeCode },
nonce,
suppressHydrationWarning: true
}
)
] });
}
function useTheme() {
const context = useContext(ThemeContext);
if (context === void 0) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}
function isTheme(value) {
return typeof value === "string" && themes.includes(value);
}
// src/theme-server.ts
var createThemeSessionResolver = (cookieThemeSession) => {
const resolver = async (request) => {
const session = await cookieThemeSession.getSession(
request.headers.get("Cookie")
);
return {
getTheme: () => {
const themeValue = session.get("theme");
return isTheme(themeValue) ? themeValue : null;
},
setTheme: (theme) => session.set("theme", theme),
commit: () => cookieThemeSession.commitSession(session),
destroy: () => cookieThemeSession.destroySession(session)
};
};
return resolver;
};
// src/create-theme-action.ts
var createThemeAction = (themeSessionResolver) => {
const action = async ({ request }) => {
const session = await themeSessionResolver(request);
const { theme } = await request.json();
if (!theme) {
return Response.json(
{ success: true },
{ headers: { "Set-Cookie": await session.destroy() } }
);
}
if (!isTheme(theme)) {
return Response.json({
success: false,
message: `theme value of ${theme} is not a valid theme.`
});
}
session.setTheme(theme);
return Response.json(
{ success: true },
{
headers: { "Set-Cookie": await session.commit() }
}
);
};
return action;
};
export {
PreventFlashOnWrongTheme,
Theme,
ThemeProvider,
createThemeAction,
createThemeSessionResolver,
isTheme,
themes,
useTheme
};