Files
ai-rpg/frontend/src/components/ui/JsonEditor.tsx

76 lines
2.0 KiB
TypeScript
Raw Normal View History

2026-06-20 19:13:05 +03:00
import { useMemo, useState } from "react";
import { cn } from "@/lib/cn";
export interface JsonEditorProps {
value: unknown;
onChange: (value: unknown) => void;
className?: string;
readOnly?: boolean;
rows?: number;
}
export function JsonEditor({ value, onChange, className, readOnly = false, rows = 16 }: JsonEditorProps) {
const initialText = useMemo(() => safeStringify(value), [value]);
const [text, setText] = useState<string>(initialText);
const [error, setError] = useState<string | null>(null);
// Re-sync external value changes if differs from current parsed text
useMemo(() => {
const external = safeStringify(value);
if (external !== text && !error) {
try {
const parsed = JSON.parse(text);
if (safeStringify(parsed) !== external) {
setText(external);
}
} catch {
// user is editing — keep text
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const next = e.target.value;
setText(next);
if (readOnly) return;
if (next.trim() === "") {
setError(null);
onChange(null);
return;
}
try {
const parsed = JSON.parse(next);
setError(null);
onChange(parsed);
} catch (err) {
setError(err instanceof Error ? err.message : "Invalid JSON");
}
};
return (
<div className={cn("w-full", className)}>
<textarea
value={text}
onChange={handleChange}
readOnly={readOnly}
rows={rows}
spellCheck={false}
className={cn(
"input font-mono text-xs",
error && "border-err focus:border-err focus:ring-err",
)}
/>
{error && <p className="mt-1 text-xs text-err">{error}</p>}
</div>
);
}
function safeStringify(v: unknown): string {
try {
return JSON.stringify(v ?? null, null, 2);
} catch {
return String(v ?? "");
}
}