feat: initial commit

This commit is contained in:
2026-06-29 23:07:14 +00:00
commit 3b9a6bc85c
152 changed files with 12558 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
import {
createContext,
useCallback,
useMemo,
useState,
type ComponentType,
type ReactNode,
} from "react";
import { idAssert } from "../../util/ts.js";
export interface PopupManagerContextData {
openPopup(popup: ComponentType<{ id: string; onClose: () => void }>): string;
openPopup<T>(
popup: ComponentType<{ id: string; onClose: () => void } & T>,
props: T
): string;
closePopup(id: string): void;
}
export const PopupManagerContext =
createContext<PopupManagerContextData | null>(null);
PopupManagerContext.displayName = "PopupManagerContext";
type Popup = {
id: string;
Component: ComponentType<{ id: string; onClose: () => void }>;
props: { id: string; onClose: () => void };
};
export function PopupManager({ children }: { children: ReactNode }) {
const [popups, setPopups] = useState<Popup[]>([]);
const openPopup = useCallback<PopupManagerContextData["openPopup"]>(
(
Component: ComponentType<{ id: string; onClose: () => void }>,
props = {}
) => {
const id = crypto.randomUUID();
Object.assign(props, {
id,
onClose: () => setPopups((prev) => prev.filter((x) => x.id !== id)),
});
idAssert<{ id: string; onClose: () => void }>(props);
setPopups((prev) => [...prev, { id, Component, props }]);
return id;
},
[]
);
const closePopup = useCallback((id: string) => {
setPopups((prev) => prev.filter((x) => x.id !== id));
}, []);
const ctx = useMemo<PopupManagerContextData>(
() => ({ openPopup, closePopup }),
[]
);
return (
<PopupManagerContext value={ctx}>
{children}
{popups.map((popup) => (
<popup.Component key={popup.id} {...popup.props} />
))}
</PopupManagerContext>
);
}