UNPKG

remix-themes

Version:

An abstraction for themes in your Remix_run app.

306 lines (295 loc) 9.71 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { PreventFlashOnWrongTheme: () => PreventFlashOnWrongTheme, Theme: () => Theme, ThemeProvider: () => ThemeProvider, createThemeAction: () => createThemeAction, createThemeSessionResolver: () => createThemeSessionResolver, isTheme: () => isTheme, themes: () => themes, useTheme: () => useTheme }); module.exports = __toCommonJS(index_exports); // src/theme-provider.tsx var import_react3 = require("react"); // src/useBroadcastChannel.ts var import_react = require("react"); function useBroadcastChannel(channelName, handleMessage, handleMessageError) { const channelRef = (0, import_react.useRef)( typeof window !== "undefined" && "BroadcastChannel" in window ? new BroadcastChannel(`${channelName}-channel`) : null ); useChannelEventListener(channelRef, "message", handleMessage); useChannelEventListener(channelRef, "messageerror", handleMessageError); return (0, import_react.useCallback)((data) => { var _a; (_a = channelRef == null ? void 0 : channelRef.current) == null ? void 0 : _a.postMessage(data); }, []); } function useChannelEventListener(channelRef, event, handler = () => { }) { (0, import_react.useEffect)(() => { const channel = channelRef.current; if (channel) { channel.addEventListener(event, handler); return () => channel.removeEventListener(event, handler); } }, [event, handler]); } // src/useCorrectCssTransition.ts var import_react2 = require("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 (0, import_react2.useCallback)( (callback) => { if (disableTransitions) { withoutTransition(() => { callback(); }); } else { callback(); } }, [disableTransitions] ); } // src/theme-provider.tsx var import_jsx_runtime = require("react/jsx-runtime"); var Theme = /* @__PURE__ */ ((Theme2) => { Theme2["DARK"] = "dark"; Theme2["LIGHT"] = "light"; return Theme2; })(Theme || {}); var themes = Object.values(Theme); var ThemeContext = (0, import_react3.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] = (0, import_react3.useState)(() => { if (specifiedTheme) { return themes.includes(specifiedTheme) ? specifiedTheme : null; } if (typeof window !== "object") return null; return getPreferredTheme(); }); const [themeDefinedBy, setThemeDefinedBy] = (0, import_react3.useState)(specifiedTheme ? "USER" : "SYSTEM"); const broadcastThemeChange = useBroadcastChannel("remix-themes", (e) => { ensureCorrectTransition(() => { console.log("broadcastThemeChange", disableTransitionOnThemeChange); setTheme(e.data.theme); setThemeDefinedBy(e.data.definedBy); }); }); (0, import_react3.useEffect)(() => { 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 = (0, import_react3.useCallback)( (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 = (0, import_react3.useMemo)( () => [theme, handleThemeChange, { definedBy: themeDefinedBy }], [theme, handleThemeChange, themeDefinedBy] ); return /* @__PURE__ */ (0, import_jsx_runtime.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__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "meta", { name: "color-scheme", content: theme === "light" ? "light dark" : "dark light" } ), ssrTheme ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "script", { dangerouslySetInnerHTML: { __html: clientThemeCode }, nonce, suppressHydrationWarning: true } ) ] }); } function useTheme() { const context = (0, import_react3.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; }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { PreventFlashOnWrongTheme, Theme, ThemeProvider, createThemeAction, createThemeSessionResolver, isTheme, themes, useTheme });