5257dfbaab
Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
121 lines
3.8 KiB
TypeScript
121 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import type { PointerEvent } from "react";
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import { t } from "@/lib/i18n";
|
|
|
|
type DebugOverlayProps = {
|
|
title?: string;
|
|
entries: Record<string, unknown>;
|
|
className?: string;
|
|
};
|
|
|
|
function formatValue(value: unknown): string {
|
|
if (value === null) return "null";
|
|
if (value === undefined) return "undefined";
|
|
if (typeof value === "string") return value;
|
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
try {
|
|
return JSON.stringify(value);
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
export function DebugOverlay({ title = t("common:debug"), entries, className }: DebugOverlayProps) {
|
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
const dragOffsetRef = useRef<{ x: number; y: number } | null>(null);
|
|
|
|
const positionRef = useRef(position);
|
|
useEffect(() => {
|
|
positionRef.current = position;
|
|
}, [position]);
|
|
|
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
const [hasCentered, setHasCentered] = useState(false);
|
|
|
|
const setContainerRef = (node: HTMLDivElement | null) => {
|
|
containerRef.current = node;
|
|
if (node && !hasCentered) {
|
|
const rect = node.getBoundingClientRect();
|
|
setPosition({
|
|
x: Math.max(12, Math.round(window.innerWidth / 2 - rect.width / 2)),
|
|
y: 24,
|
|
});
|
|
setHasCentered(true);
|
|
}
|
|
};
|
|
|
|
const clampPosition = (x: number, y: number) => {
|
|
if (!containerRef.current) {
|
|
return { x, y };
|
|
}
|
|
const rect = containerRef.current.getBoundingClientRect();
|
|
const maxX = Math.max(12, window.innerWidth - rect.width - 12);
|
|
const maxY = Math.max(12, window.innerHeight - rect.height - 12);
|
|
return {
|
|
x: Math.min(Math.max(12, x), maxX),
|
|
y: Math.min(Math.max(12, y), maxY),
|
|
};
|
|
};
|
|
|
|
const displayEntries = useMemo(
|
|
() =>
|
|
Object.entries(entries).map(([key, value]) => ({
|
|
key,
|
|
value: formatValue(value),
|
|
})),
|
|
[entries],
|
|
);
|
|
|
|
const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
|
|
dragOffsetRef.current = {
|
|
x: event.clientX - positionRef.current.x,
|
|
y: event.clientY - positionRef.current.y,
|
|
};
|
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
};
|
|
|
|
const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {
|
|
if (!dragOffsetRef.current) return;
|
|
const nextX = event.clientX - dragOffsetRef.current.x;
|
|
const nextY = event.clientY - dragOffsetRef.current.y;
|
|
setPosition(clampPosition(nextX, nextY));
|
|
};
|
|
|
|
const handlePointerUp = (event: PointerEvent<HTMLDivElement>) => {
|
|
dragOffsetRef.current = null;
|
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"fixed z-50 min-w-[220px] max-w-[360px] rounded-lg border border-border bg-background/45 p-3 text-xs shadow-lg",
|
|
className,
|
|
)}
|
|
style={{ left: position.x, top: position.y }}
|
|
ref={setContainerRef}
|
|
>
|
|
<div
|
|
className="flex items-center justify-between gap-2 cursor-move select-none border-b border-border/60 pb-2 mb-2 text-[11px] uppercase tracking-wide text-muted-foreground"
|
|
onPointerDown={handlePointerDown}
|
|
onPointerMove={handlePointerMove}
|
|
onPointerUp={handlePointerUp}
|
|
>
|
|
<span>{title}</span>
|
|
<span className="text-[10px] opacity-70">drag</span>
|
|
</div>
|
|
<div className="space-y-1">
|
|
{displayEntries.map((entry) => (
|
|
<div key={entry.key} className="flex justify-between gap-2">
|
|
<span className="text-muted-foreground">{entry.key}</span>
|
|
<span className="text-foreground text-right break-words">{entry.value}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|