Add AC/BC RTCP programs and 3D machine manager

This commit is contained in:
2026-09-20 12:31:12 +08:00
parent 1c1e5f9672
commit 8aab531b02
363 changed files with 53213 additions and 58 deletions

View File

@@ -0,0 +1,55 @@
import { useEffect, useId, useLayoutEffect, useRef, type PointerEvent, type ReactNode } from "react";
type Props = { open: boolean; title: string; className?: string; onClose: () => void; children: ReactNode };
// Native dialog provides focus containment, inert background, Escape and focus return.
// Keeping children mounted preserves drafts and pending import results when closed.
export function DraggableDialog({ open, title, className = "", onClose, children }: Props) {
const ref = useRef<HTMLDialogElement>(null);
const titleId = useId();
const drag = useRef<{ id: number; x: number; y: number; left: number; top: number } | null>(null);
const position = (left: number, top: number) => {
const dialog = ref.current;
if (!dialog) return;
const bounds = dialog.getBoundingClientRect();
const width = document.documentElement.clientWidth, height = window.innerHeight;
dialog.style.left = `${Math.max(8, Math.min(left, width - bounds.width - 8))}px`;
dialog.style.top = `${Math.max(8, Math.min(top, height - bounds.height - 8))}px`;
};
useLayoutEffect(() => {
const dialog = ref.current!;
if (!open) { if (dialog.open) dialog.close(); return; }
if (!dialog.open) dialog.showModal();
const bounds = dialog.getBoundingClientRect();
position((document.documentElement.clientWidth - bounds.width) / 2, (window.innerHeight - bounds.height) / 2);
const keepVisible = () => { const rect = dialog.getBoundingClientRect(); position(rect.left, rect.top); };
const observer = new ResizeObserver(keepVisible);
observer.observe(dialog); window.addEventListener("resize", keepVisible);
return () => { observer.disconnect(); window.removeEventListener("resize", keepVisible); drag.current = null; };
}, [open]);
useEffect(() => () => { if (ref.current?.open) ref.current.close(); }, []);
const start = (event: PointerEvent<HTMLElement>) => {
if (event.button !== 0 || (event.target as HTMLElement).closest("button")) return;
const bounds = ref.current!.getBoundingClientRect();
drag.current = { id: event.pointerId, x: event.clientX, y: event.clientY, left: bounds.left, top: bounds.top };
event.currentTarget.setPointerCapture(event.pointerId);
};
const move = (event: PointerEvent<HTMLElement>) => {
const from = drag.current;
if (from?.id !== event.pointerId) return;
position(from.left + event.clientX - from.x, from.top + event.clientY - from.y);
};
const stop = () => { drag.current = null; };
return <dialog ref={ref} className={`machine-dialog ${className}`} aria-modal="true" aria-labelledby={titleId} onCancel={(event) => { event.preventDefault(); onClose(); }} onKeyDown={(event) => {
if (event.key !== "Tab") return;
const elements = [...event.currentTarget.querySelectorAll<HTMLElement>('button:not(:disabled), a[href], input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), summary, [tabindex="0"]')].filter((element) => element.getClientRects().length > 0);
const first = elements[0], last = elements.at(-1);
if (event.shiftKey && (document.activeElement === first || !elements.includes(document.activeElement as HTMLElement))) { event.preventDefault(); last?.focus(); }
else if (!event.shiftKey && (document.activeElement === last || !elements.includes(document.activeElement as HTMLElement))) { event.preventDefault(); first?.focus(); }
}}>
<header className="machine-dialog-titlebar" onPointerDown={start} onPointerMove={move} onPointerUp={stop} onPointerCancel={stop} onLostPointerCapture={stop}>
<strong id={titleId}>{title}</strong><button type="button" autoFocus aria-label={`关闭${title}`} onClick={onClose}>×</button>
</header>
<div className="machine-dialog-body">{children}</div>
</dialog>;
}