remix-themes
Version:
An abstraction for themes in your Remix_run app.
306 lines (295 loc) • 9.71 kB
JavaScript
;
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
});