Initial commit

This commit is contained in:
Basiclean committed 2026-05-02 10:41:00 +02:00
commit bcca176137
88 files changed
+8725

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
"use client";
import { useLayoutEffect } from "react";
type Mode = "scroll" | "offset";
export function useHeightVar(
ref: React.RefObject<HTMLElement | null>,
cssVarName: string,
mode: Mode = "scroll",
trigger?: unknown,
) {
useLayoutEffect(() => {
const el = ref.current;
if (!el) return;
const measure = () => {
const h = mode === "scroll" ? el.scrollHeight : el.offsetHeight;
el.style.setProperty(cssVarName, `${h}px`);
};
measure();
const ro = new ResizeObserver(measure);
ro.observe(el);
return () => ro.disconnect();
}, [ref, cssVarName, mode, trigger]);
}
+25
View File
@@ -0,0 +1,25 @@
"use client";
import { useEffect, useState } from "react";
export function useIsDesktop(breakpoint = 1024) {
const [isDesktop, setIsDesktop] = useState(false);
useEffect(() => {
const mql = window.matchMedia(`(min-width: ${breakpoint}px)`);
const update = () => setIsDesktop(mql.matches);
update();
if (mql.addEventListener) {
mql.addEventListener("change", update);
return () => mql.removeEventListener("change", update);
}
mql.addListener(update);
return () => mql.removeListener(update);
}, [breakpoint]);
return isDesktop;
}
+95
View File
@@ -0,0 +1,95 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
type Options = {
supportsHover: boolean;
desktopBp?: number;
};
export function useNavToggleMenu({ desktopBp = 1024 }: Options) {
const [menuOpen, setMenuOpen] = useState(false);
const [openSubmenuId, setOpenSubmenuId] = useState<string | null>(null);
const [noTransition, setNoTransition] = useState(false);
const rafRef = useRef<number | null>(null);
const clearRaf = useCallback(() => {
if (rafRef.current != null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
}, []);
const closeMenu = useCallback(() => {
setMenuOpen(false);
setOpenSubmenuId(null);
}, []);
const closeInstantNoAnim = useCallback(() => {
clearRaf();
setNoTransition(true);
closeMenu();
rafRef.current = requestAnimationFrame(() => {
setNoTransition(false);
rafRef.current = null;
});
}, [clearRaf, closeMenu]);
const toggleMenu = useCallback(() => {
clearRaf();
setNoTransition(false);
setMenuOpen((prev) => {
const next = !prev;
if (!next) {
setOpenSubmenuId(null);
}
return next;
});
}, [clearRaf]);
const toggleSubmenu = useCallback((id: string) => {
setOpenSubmenuId((cur) => (cur === id ? null : id));
}, []);
useEffect(() => {
const mql = window.matchMedia(`(min-width: ${desktopBp}px)`);
const onChange = () => {
clearRaf();
setNoTransition(true);
closeMenu();
requestAnimationFrame(() => setNoTransition(false));
};
if (mql.addEventListener) {
mql.addEventListener("change", onChange);
} else {
mql.addListener(onChange);
}
return () => {
if (mql.removeEventListener) {
mql.removeEventListener("change", onChange);
} else {
mql.removeListener(onChange);
}
};
}, [desktopBp, clearRaf, closeMenu]);
useEffect(() => clearRaf, [clearRaf]);
return {
menuOpen,
openSubmenuId,
noTransition,
setMenuOpen,
setOpenSubmenuId,
toggleMenu,
toggleSubmenu,
closeMenu,
closeInstantNoAnim,
};
}
+25
View File
@@ -0,0 +1,25 @@
"use client";
import { useEffect, useState } from "react";
export function useSupportsHover() {
const [supportsHover, setSupportsHover] = useState(false);
useEffect(() => {
const mql = window.matchMedia("(hover: hover) and (pointer: fine)");
const update = () => setSupportsHover(mql.matches);
update();
if (mql.addEventListener) {
mql.addEventListener("change", update);
return () => mql.removeEventListener("change", update);
}
mql.addListener(update);
return () => mql.removeListener(update);
}, []);
return supportsHover;
}
+25
View File
@@ -0,0 +1,25 @@
"use client";
import { useEffect } from "react";
type Options = {
className?: string;
};
export function useToggleBodyOverflow(locked: boolean, options: Options = {}) {
const { className = "has-menu-open" } = options;
useEffect(() => {
const root = document.documentElement;
root.classList.toggle(className, locked);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = locked ? "hidden" : "";
return () => {
root.classList.remove(className);
document.body.style.overflow = prevOverflow;
};
}, [locked, className]);
}