"use client"; import ReactJson, { type InteractionProps } from "@microlink/react-json-view"; import { useTheme } from "next-themes"; import { useCallback, useMemo } from "react"; /** * Shared JSON viewer/editor wrapper around @microlink/react-json-view. * * One component, dual mode: passing ``editable`` + ``onChange`` enables * inline value editing, key renaming, add and delete. Omitting them * yields a read-only viewer. The underlying library is uncontrolled — it * mutates its own internal copy of ``src`` and surfaces the final tree on * each interaction via ``updated_src``, which we forward to ``onChange``. * * Theme follows ``next-themes``: a dark base-16 palette in dark mode, the * library's neutral default in light mode. Defaults are tuned for our * compact UI surfaces (no data-type labels, no key quotes, triangle icons, * tight indent). */ export interface JsonViewProps { /** The JSON value to display. Primitives are wrapped under ``{ value }`` * because the underlying library requires an object root. */ src: unknown; /** Enables value/key editing + add + delete. Requires ``onChange`` to * observe the result; without it the toggle is silently a no-op. */ editable?: boolean; /** Called with the full updated tree on every accepted interaction. */ onChange?: (next: unknown) => void; /** Collapse depth. ``true`` collapses everything past the root; a number * collapses from that depth onward. */ collapsed?: boolean | number; /** Root label. Default ``false`` (no label — saves vertical space). */ name?: string | false; className?: string; } /** Recursively coerce string values that are valid JSON numbers back to numbers. * react-json-view's text input always yields strings; this restores the * correct type so filters like ``{ "folder_id": 56 }`` survive editing. */ function coerceNumbers(value: unknown): unknown { if (typeof value === "string") { const n = Number(value); return !Number.isNaN(n) && value.trim() !== "" ? n : value; } if (Array.isArray(value)) return value.map(coerceNumbers); if (value && typeof value === "object") { return Object.fromEntries( Object.entries(value as Record).map(([k, v]) => [k, coerceNumbers(v)]) ); } return value; } const DARK_THEME = "monokai" as const; const LIGHT_THEME = "rjv-default" as const; const SHARED_DEFAULTS = { iconStyle: "triangle" as const, indentWidth: 2, enableClipboard: true, displayDataTypes: false, displayObjectSize: true, quotesOnKeys: false, collapseStringsAfterLength: 80, }; export function JsonView({ src, editable = false, onChange, collapsed = 2, name = false, className, }: JsonViewProps) { const { resolvedTheme } = useTheme(); const theme = resolvedTheme === "dark" ? DARK_THEME : LIGHT_THEME; // The library throws on non-object roots. Wrap primitives and null/undefined. const safeSrc = useMemo(() => { if (src && typeof src === "object") return src as object; return { value: src }; }, [src]); const handleChange = useCallback( (interaction: InteractionProps) => { onChange?.(coerceNumbers(interaction.updated_src)); return true; }, [onChange] ); const interactive = editable && onChange ? handleChange : (false as const); return (
); }