create-vanjs
Version:
🍦 Quick tool for scaffolding your first VanJS project
228 lines (212 loc) • 5.94 kB
JavaScript
import van from "vanjs-core";
import { Moon, Sun, SunMoon } from "vanjs-lucide";
import { persistentState } from "../../util/persistentState";
const isClient = () => typeof window !== "undefined";
const getSystemTheme = () => {
if (isClient() && globalThis?.matchMedia) {
return globalThis?.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
// return the default
return "light";
};
// create a persisten state of the system theme
const systemTheme = persistentState("ui-theme", getSystemTheme());
export const ThemeController = (
{ theme, ...props },
...children
) => {
if (!theme) {
throw new Error(
"ThemeController requires a theme property with valid value",
);
}
const { form } = van.tags;
const isConnected = van.state(false);
// the controller form
const controllerForm = form(
props,
...children,
);
/**
* @param {MediaQueryListEvent} event
*/
const themeCallback = (event) => {
const newTheme = event.matches ? "dark" : "light";
if (systemTheme.val === "system") {
theme.val = newTheme;
}
};
van.derive(() => {
if (!isClient()) return;
document.documentElement.setAttribute("data-theme", systemTheme.val);
});
van.derive(() => {
if (!isClient()) return;
if (isConnected.val) {
globalThis?.matchMedia("(prefers-color-scheme: dark)").addEventListener(
"change",
themeCallback,
);
} else {
globalThis?.matchMedia("(prefers-color-scheme: dark)")
.removeEventListener(
"change",
themeCallback,
);
}
});
van.derive(() => {
setTimeout(() => isConnected.val = controllerForm.isConnected);
});
return controllerForm;
};
export const ThemeToggle = (
initialProps,
) => {
const { input, label, span, button } = van.tags;
const props = Object.fromEntries(
Object.entries(initialProps).filter(([_, val]) => val),
);
const themes = ["light", "dark", "system"];
const themeIndex = van.state(themes.indexOf(systemTheme.val));
// the internal theme state
const theme = van.state(themes[themeIndex.val]);
const icon = van.derive(() => {
const currentTheme = theme.val;
if (currentTheme === "dark") {
return { icon: Moon({ class: "h-6 w-6" }) };
} else if (currentTheme === "light") {
return { icon: Sun({ class: "h-6 w-6" }) };
}
return { icon: SunMoon({ class: "h-6 w-6" }) };
});
return ThemeController(
{
...props,
theme,
},
button(
{
class: "btn btn-ghost btn-square",
type: "button",
onclick: () => {
const oldVal = themeIndex.oldVal;
const newIdx = oldVal < 2 ? themeIndex.oldVal + 1 : 0;
themeIndex.val = newIdx;
theme.val = themes[newIdx];
systemTheme.val = themes[newIdx];
},
},
icon.val.icon,
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: () => systemTheme.val === "light",
value: "light",
}),
input({
type: "radio",
name: "theme-buttons",
class: "theme-controller",
"aria-label": "Dard",
checked: () => systemTheme.val === "dark",
value: "dark",
}),
input({
type: "radio",
name: "theme-buttons",
class: "theme-controller",
"aria-label": "System",
checked: () => systemTheme.val === "system",
value: "system",
}),
),
);
};
export const ThemeDropdown = (props) => {
const { input, div, ul, li, button } = van.tags;
const themes = ["light", "dark", "system"];
const themeIndex = van.state(themes.indexOf(systemTheme.val));
// the internal theme state
const theme = van.state(themes[themeIndex.val]);
const icon = van.derive(() => {
const currentTheme = theme.val;
if (currentTheme === "dark") {
return Moon({ class: "h-6 w-6 mr-2" });
} else if (currentTheme === "light") {
return Sun({ class: "h-6 w-6 mr-2" });
}
return SunMoon({ class: "h-6 w-6 mr-2" });
});
const onSelect = (e) => {
const value = e.target.value;
theme.val = value;
systemTheme.val = value;
};
return ThemeController(
{
...props,
theme,
},
div(
{ class: "dropdown dropdown-end" },
button(
{ type: "button", class: "btn btn-ghost" },
icon,
"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": "Default",
checked: () => systemTheme.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: () => systemTheme.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: () => systemTheme.val === "system",
value: "system",
}),
),
),
),
);
};