initial commit of v5

This commit is contained in:
Amruth Pillai
2026-01-19 23:31:54 +01:00
parent 55bdfd0067
commit cad390fa13
1132 changed files with 200807 additions and 165288 deletions
+28
View File
@@ -0,0 +1,28 @@
import { useLingui } from "@lingui/react";
import { useRouter } from "@tanstack/react-router";
import { isTheme, setThemeServerFn, themeMap } from "@/utils/theme";
import { Combobox, type ComboboxProps } from "../ui/combobox";
import { useTheme } from "./provider";
type Props = Omit<ComboboxProps, "options" | "value" | "onValueChange">;
export function ThemeCombobox(props: Props) {
const router = useRouter();
const { i18n } = useLingui();
const { theme, setTheme } = useTheme();
const options = Object.entries(themeMap).map(([value, label]) => ({
value,
label: i18n.t(label),
keywords: [i18n.t(label)],
}));
const onThemeChange = async (value: string | null) => {
if (!value || !isTheme(value)) return;
await setThemeServerFn({ data: value });
setTheme(value);
router.invalidate();
};
return <Combobox options={options} defaultValue={theme} onValueChange={onThemeChange} {...props} />;
}
+49
View File
@@ -0,0 +1,49 @@
import { useRouter } from "@tanstack/react-router";
import { createContext, type PropsWithChildren, use } from "react";
import { setThemeServerFn, type Theme } from "@/utils/theme";
type ThemeContextValue = {
theme: Theme;
setTheme: (value: Theme, options?: { playSound?: boolean }) => void;
toggleTheme: (options?: { playSound?: boolean }) => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
type Props = PropsWithChildren<{ theme: Theme }>;
export function ThemeProvider({ children, theme }: Props) {
const router = useRouter();
async function setTheme(value: Theme, options: { playSound?: boolean } = {}) {
const { playSound = true } = options;
document.documentElement.classList.toggle("dark", value === "dark");
await setThemeServerFn({ data: value });
router.invalidate();
if (!playSound) return;
try {
const soundClip = value === "dark" ? "/sounds/switch-off.mp3" : "/sounds/switch-on.mp3";
const audio = new Audio(soundClip);
await audio.play();
} catch {
// ignore errors
}
}
function toggleTheme(options: { playSound?: boolean } = {}) {
setTheme(theme === "dark" ? "light" : "dark", options);
}
return <ThemeContext value={{ theme, setTheme, toggleTheme }}>{children}</ThemeContext>;
}
export function useTheme() {
const value = use(ThemeContext);
if (!value) throw new Error("useTheme must be used within a ThemeProvider");
return value;
}
+65
View File
@@ -0,0 +1,65 @@
import { t } from "@lingui/core/macro";
import { MoonIcon, SunIcon } from "@phosphor-icons/react";
import { useCallback, useRef } from "react";
import { flushSync } from "react-dom";
import { Button } from "@/components/animate-ui/components/buttons/button";
import { useTheme } from "./provider";
export function ThemeToggleButton(props: React.ComponentProps<typeof Button>) {
const { theme, toggleTheme } = useTheme();
const buttonRef = useRef<HTMLButtonElement>(null);
const onToggleTheme = useCallback(async () => {
if (
!buttonRef.current ||
!document.startViewTransition ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
toggleTheme();
return;
}
let timeout: NodeJS.Timeout;
const style = document.createElement("style");
style.textContent = `
::view-transition-old(root), ::view-transition-new(root) {
mix-blend-mode: normal !important;
animation: none !important;
}
`;
function transitionCallback() {
flushSync(() => {
toggleTheme();
timeout = setTimeout(() => {
clearTimeout(timeout);
document.head.removeChild(style);
}, 1000);
});
}
document.head.appendChild(style);
await document.startViewTransition(transitionCallback).ready;
const { top, left, width, height } = buttonRef.current.getBoundingClientRect();
const x = left + width / 2;
const y = top + height / 2;
const right = window.innerWidth - left;
const bottom = window.innerHeight - top;
const maxRadius = Math.hypot(Math.max(left, right), Math.max(top, bottom));
document.documentElement.animate(
{ clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${maxRadius}px at ${x}px ${y}px)`] },
{ duration: 500, easing: "ease-in-out", pseudoElement: "::view-transition-new(root)" },
);
}, [toggleTheme]);
const ariaLabel = theme === "dark" ? t`Switch to light theme` : t`Switch to dark theme`;
return (
<Button size="icon" variant="ghost" ref={buttonRef} onClick={onToggleTheme} aria-label={ariaLabel} {...props}>
{theme === "dark" ? <MoonIcon aria-hidden="true" /> : <SunIcon aria-hidden="true" />}
</Button>
);
}