UNPKG

create-vanjs

Version:

🍦 Quick tool for scaffolding your first VanJS project

218 lines (199 loc) 5.88 kB
import van from "vanjs-core"; import { ChildDom, Props, PropsWithKnownKeys, PropValueOrDerived, } from "vanjs-core"; import { Moon, Sun, SunMoon } from "vanjs-lucide"; import { persistentState } from "../../util/persistentState"; const isClient = () => typeof window !== "undefined"; type ChangeEvent<T extends EventTarget & Element = HTMLInputElement> = & InputEvent & { target: T }; const mediaTarget = isClient() && globalThis ? globalThis.matchMedia("(prefers-color-scheme: dark)") : undefined; const getSystemTheme = () => { if (isClient() && globalThis && "matchMedia" in globalThis) { return !mediaTarget?.matches ? "light" : "dark"; } // return the default return "dark"; }; type Theme = "dark" | "light" | "system"; type ThemeControllerProps = & Props // & { theme: State<Theme> } & Record<string, PropValueOrDerived> & PropsWithKnownKeys<HTMLFormElement>; // create a persisten state of the system theme const currentTheme = persistentState<Theme>("ui-theme", "system"); const systemTheme = van.state<Theme>(getSystemTheme()); export const ThemeController = ( { ...rest }: ThemeControllerProps, ...children: ChildDom[] ) => { const props = Object.fromEntries( Object.entries(rest).filter(([_, val]) => val !== undefined), ) as ThemeControllerProps; const { form } = van.tags; // the controller form const controllerForm = form( props, ...children, ); /** * @param {MediaQueryListEvent} event */ const themeCallback = (event: MediaQueryListEvent) => { const newTheme = event.matches ? "dark" : "light"; systemTheme.val = newTheme; }; // Fix 1: resolve actual theme for data-theme attribute van.derive(() => { if (!isClient()) return; const resolved = currentTheme.val === "system" ? systemTheme.val : currentTheme.val; document.documentElement.setAttribute("data-theme", resolved); }); van.derive(() => { if (!isClient()) return; mediaTarget?.addEventListener("change", themeCallback); }); return controllerForm; }; export const ThemeToggle = ( initialProps: Omit<ThemeControllerProps, "theme">, ) => { const { input, label, span, button } = van.tags; const props = Object.fromEntries( Object.entries(initialProps).filter(([_, val]) => val !== undefined), ) as ThemeControllerProps; const themes: Theme[] = ["light", "dark", "system"]; return ThemeController( props, button( { class: "btn btn-ghost btn-square", type: "button", onclick: () => { const idx = themes.indexOf(currentTheme.val); currentTheme.val = themes[(idx + 1) % 3]; }, }, () => { const theme = currentTheme.val; if (theme === "dark") { return Moon({ class: "h-6 w-6" }); } else if (theme === "light") { return Sun({ class: "h-6 w-6" }); } return SunMoon({ class: "h-6 w-6" }); }, span({ class: "sr-only" }, "Toggle Theme"), ), label( { class: "sr-only", for: "theme-buttons" }, "Toggle Theme", input({ type: "radio", name: "theme-buttons", class: "theme-controller", "aria-label": "Light", checked: () => currentTheme.val === "light", value: "light", }), input({ type: "radio", name: "theme-buttons", class: "theme-controller", "aria-label": "Dark", checked: () => currentTheme.val === "dark", value: "dark", }), input({ type: "radio", name: "theme-buttons", class: "theme-controller", "aria-label": "System", checked: () => currentTheme.val === "system", value: "system", }), ), ); }; export const ThemeDropdown = (props: Omit<ThemeControllerProps, "theme">) => { const { input, div, ul, li, button, span } = van.tags; const onSelect = (e: ChangeEvent) => { const value = e.target.value as Theme; currentTheme.val = value; }; return ThemeController( props, div( { class: "dropdown dropdown-end" }, button( { type: "button", class: "btn btn-ghost btn-square", ariaLabel: "Theme", }, () => { const theme = currentTheme.val; if (theme === "dark") return Moon({ class: "h-6 w-6" }); if (theme === "light") return Sun({ class: "h-6 w-6" }); return SunMoon({ class: "h-6 w-6" }); }, span( { class: "sr-only" }, "Theme", ), ), ul( { tabindex: "0", class: "dropdown-content bg-base-300 rounded-box z-1 w-52 p-2 shadow-2xl", }, li( input({ type: "radio", name: "theme-dropdown", onchange: onSelect, class: "theme-controller btn btn-sm btn-block btn-ghost justify-start", "aria-label": "Light", checked: () => currentTheme.val === "light", value: "light", }), ), li( input({ type: "radio", name: "theme-dropdown", onchange: onSelect, class: "theme-controller btn btn-sm btn-block btn-ghost justify-start", "aria-label": "Dark", checked: () => currentTheme.val === "dark", value: "dark", }), ), li( input({ type: "radio", name: "theme-dropdown", onchange: onSelect, class: "theme-controller btn btn-sm btn-block btn-ghost justify-start", "aria-label": "System", checked: () => currentTheme.val === "system", value: "system", }), ), ), ), ); };