Changed the behavior of the 'more' bar into something less annoying, and re-did the entire project grid component chain.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useState, useEffect, useRef, type CSSProperties } from "react";
|
||||
|
||||
export function useStaggerReveal(count: number, baseDelay = 80) {
|
||||
const [visible, setVisible] = useState<Set<number>>(new Set());
|
||||
@@ -21,6 +21,101 @@ export function useStaggerReveal(count: number, baseDelay = 80) {
|
||||
return visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the rendered width of an element. `mounted` stays false until the
|
||||
* first measurement lands, so callers can avoid rendering a width-dependent
|
||||
* layout against a zero/SSR width.
|
||||
*/
|
||||
export function useElementWidth<T extends HTMLElement = HTMLDivElement>() {
|
||||
const ref = useRef<T>(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const w = entries[0]?.contentRect.width ?? 0;
|
||||
setWidth(w);
|
||||
setMounted(true);
|
||||
});
|
||||
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return { width, ref, mounted };
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets a nested element break out to the full width of the viewport.
|
||||
*
|
||||
* The pure-CSS `left:50%; margin-left:-50vw` trick only works when every
|
||||
* ancestor is horizontally centered, which is not true here -- Profile's
|
||||
* content column is left-aligned inside the centered page wrapper. Measuring
|
||||
* the real offset is exact from any nesting depth, and using
|
||||
* `documentElement.clientWidth` instead of `100vw` excludes the scrollbar,
|
||||
* so this can't induce a horizontal scroll.
|
||||
*
|
||||
* Returns a style that is a no-op until the first measurement lands.
|
||||
*/
|
||||
export function useFullBleed<T extends HTMLElement = HTMLDivElement>() {
|
||||
const ref = useRef<T>(null);
|
||||
const [bleed, setBleed] = useState<{ width: number; offsetLeft: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const measure = () => {
|
||||
const width = document.documentElement.clientWidth;
|
||||
// Undo any margin this element is already carrying, so re-measuring is idempotent.
|
||||
const current = parseFloat(el.style.marginLeft || "0") || 0;
|
||||
const offsetLeft = el.getBoundingClientRect().left - current;
|
||||
|
||||
setBleed((prev) =>
|
||||
prev && prev.width === width && Math.abs(prev.offsetLeft - offsetLeft) < 0.5
|
||||
? prev
|
||||
: { width, offsetLeft }
|
||||
);
|
||||
};
|
||||
|
||||
measure();
|
||||
|
||||
const observer = new ResizeObserver(measure);
|
||||
observer.observe(document.documentElement);
|
||||
window.addEventListener("resize", measure);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("resize", measure);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const style: CSSProperties = bleed
|
||||
? { width: bleed.width, marginLeft: -bleed.offsetLeft }
|
||||
: { width: "100%", marginLeft: 0 };
|
||||
|
||||
return { ref, style, measured: bleed !== null };
|
||||
}
|
||||
|
||||
/** Mirrors the user's `prefers-reduced-motion` setting, and follows changes to it. */
|
||||
export function usePrefersReducedMotion() {
|
||||
const [reduced, setReduced] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
const sync = () => setReduced(query.matches);
|
||||
|
||||
sync();
|
||||
query.addEventListener("change", sync);
|
||||
return () => query.removeEventListener("change", sync);
|
||||
}, []);
|
||||
|
||||
return reduced;
|
||||
}
|
||||
|
||||
export function useMountTransition(delay = 50) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
@@ -31,63 +126,3 @@ export function useMountTransition(delay = 50) {
|
||||
|
||||
return mounted;
|
||||
}
|
||||
|
||||
|
||||
export function useWiggle(durationMs = 1500, onComplete: () => void) {
|
||||
const [wiggling, setWiggling] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const startTime = useRef<number>(0);
|
||||
const rafId = useRef<number>(0);
|
||||
const completeTimer = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
const completedRef = useRef(false);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
setWiggling(false);
|
||||
setProgress(0);
|
||||
cancelAnimationFrame(rafId.current);
|
||||
if (completeTimer.current) clearTimeout(completeTimer.current);
|
||||
}, []);
|
||||
|
||||
const tick = useCallback(() => {
|
||||
const elapsed = Date.now() - startTime.current;
|
||||
const p = Math.min(1, elapsed / durationMs);
|
||||
setProgress(p);
|
||||
if (p < 1) {
|
||||
rafId.current = requestAnimationFrame(tick);
|
||||
}
|
||||
}, [durationMs]);
|
||||
|
||||
const start = useCallback(() => {
|
||||
completedRef.current = false;
|
||||
startTime.current = Date.now();
|
||||
setWiggling(true);
|
||||
setProgress(0);
|
||||
rafId.current = requestAnimationFrame(tick);
|
||||
|
||||
completeTimer.current = setTimeout(() => {
|
||||
completedRef.current = true;
|
||||
stop();
|
||||
onComplete();
|
||||
}, durationMs);
|
||||
}, [durationMs, onComplete, tick, stop]);
|
||||
|
||||
const release = useCallback(() => {
|
||||
if (!completedRef.current) stop();
|
||||
}, [stop]);
|
||||
|
||||
// cleanup on unmount
|
||||
useEffect(() => () => {
|
||||
cancelAnimationFrame(rafId.current);
|
||||
if (completeTimer.current) clearTimeout(completeTimer.current);
|
||||
}, []);
|
||||
|
||||
const handlers = {
|
||||
onPointerDown: start,
|
||||
onPointerEnter: start,
|
||||
onPointerUp: release,
|
||||
onPointerLeave: release,
|
||||
};
|
||||
|
||||
return { wiggling, progress, handlers };
|
||||
}
|
||||
Reference in New Issue
Block a user