'use client'
import { useState } from 'react'
import { cn } from '../../lib/cn'
import { ChevronRightIcon } from '../internal/icons'
import { Surface } from '../Surface'
export interface JsonViewerProps {
/** Any JSON-serialisable value. */
data: unknown
/** Accessible name for the viewer. */
label: string
/** How many levels start open. */
defaultExpandDepth?: number
/** Name shown for the root. Omit to show the root's contents directly. */
rootName?: string
/** Merged last, so it wins. */
className?: string
}
/** The code palette, so JSON here and in a CodeBlock read the same. */
const COLOR = {
string: 'var(--syntax-string)',
number: 'var(--syntax-number)',
literal: 'var(--syntax-keyword)',
}
function Primitive({ value }: { value: unknown }) {
if (typeof value === 'string') return "{value}"
if (typeof value === 'number') return {String(value)}
if (typeof value === 'boolean' || value === null) {
return {String(value)}
}
return {String(value)}
}
function Key({ name, quoted }: { name: string; quoted: boolean }) {
return (
<>
{quoted ? `"${name}"` : name}:
>
)
}
function Branch({
name,
quoted,
value,
depth,
expandDepth,
}: {
name?: string
quoted: boolean
value: unknown
depth: number
expandDepth: number
}) {
const nested = value !== null && typeof value === 'object'
const [open, setOpen] = useState(depth < expandDepth)
if (!nested) {
return (
)
}
/**
* A payload, as a collapsible tree — an API response, a webhook body, a log line.
*
* Each object and array is a disclosure button with aria-expanded, so the tree
* is navigable by Tab and announced as open or closed without the full ARIA
* tree pattern a reader would have to learn. Values take the code palette, so
* JSON here and JSON in a CodeBlock read the same, and nothing is truncated:
* collapsing hides a branch, it never cuts a value short.
*/
export function JsonViewer({
data,
label,
defaultExpandDepth = 1,
rootName,
className,
}: JsonViewerProps) {
const top =
rootName === undefined && data !== null && typeof data === 'object'
? Array.isArray(data)
? data.map((item, index) => [String(index), item] as [string, unknown])
: Object.entries(data as Record)
: null
return (