feat: add settings menu
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m40s
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m40s
This commit is contained in:
121
src/components/lift/Liftable.tsx
Normal file
121
src/components/lift/Liftable.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useLift } from "./LiftContext";
|
||||
|
||||
type LiftableProps = {
|
||||
id?: string;
|
||||
always?: boolean;
|
||||
};
|
||||
|
||||
type Props = LiftableProps & {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function useLiftable<T extends Record<string, unknown> = Record<string, never>>(
|
||||
render: (props: { isLifted: boolean } & T) => ReactNode,
|
||||
options?: LiftableProps & T,
|
||||
): { id: string; element: ReactNode } {
|
||||
const autoId = useId();
|
||||
const {
|
||||
id: idProp,
|
||||
always,
|
||||
...extraProps
|
||||
} = (options ?? {}) as LiftableProps & Record<string, unknown>;
|
||||
const id = idProp ?? autoId;
|
||||
const { liftedIds, alwaysLiftedIds, registerAlways, unregisterAlways } = useLift();
|
||||
|
||||
useEffect(() => {
|
||||
if (always) {
|
||||
registerAlways(id);
|
||||
return () => unregisterAlways(id);
|
||||
}
|
||||
}, [always, id, registerAlways, unregisterAlways]);
|
||||
|
||||
const isLifted = liftedIds.has(id) || alwaysLiftedIds.has(id);
|
||||
|
||||
const element = (
|
||||
<Liftable id={id}>{render({ isLifted, ...extraProps } as { isLifted: boolean } & T)}</Liftable>
|
||||
);
|
||||
|
||||
return { id, element };
|
||||
}
|
||||
|
||||
export function Liftable({ id: idProp, always, children }: Props) {
|
||||
const autoId = useId();
|
||||
const id = idProp ?? autoId;
|
||||
const { liftedIds, alwaysLiftedIds, registerAlways, unregisterAlways, portalContainer } =
|
||||
useLift();
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const [rect, setRect] = useState<DOMRect | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (always) {
|
||||
registerAlways(id);
|
||||
return () => unregisterAlways(id);
|
||||
}
|
||||
}, [always, id, registerAlways, unregisterAlways]);
|
||||
|
||||
const isLifted = liftedIds.has(id) || alwaysLiftedIds.has(id);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isLifted && wrapperRef.current) {
|
||||
setRect(wrapperRef.current.getBoundingClientRect());
|
||||
}
|
||||
if (!isLifted) {
|
||||
setRect(null);
|
||||
}
|
||||
}, [isLifted]);
|
||||
|
||||
// Re-measure on resize while lifted
|
||||
useEffect(() => {
|
||||
if (!isLifted || !wrapperRef.current) return;
|
||||
|
||||
const measure = () => {
|
||||
if (wrapperRef.current) {
|
||||
setRect(wrapperRef.current.getBoundingClientRect());
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(measure);
|
||||
observer.observe(wrapperRef.current);
|
||||
window.addEventListener("resize", measure);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("resize", measure);
|
||||
};
|
||||
}, [isLifted]);
|
||||
|
||||
// When lifted and we have measurements + portal target, render in portal
|
||||
if (isLifted && rect && portalContainer) {
|
||||
return (
|
||||
<>
|
||||
{/* Placeholder preserves layout space */}
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
style={{ width: rect.width, height: rect.height, visibility: "hidden" }}
|
||||
/>
|
||||
{/* Portal children above blur */}
|
||||
{createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
portalContainer,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Normal inline rendering
|
||||
return <div ref={wrapperRef}>{children}</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user