11'use client' ;
22
33import { CaretDownIcon , CaretRightIcon } from '@phosphor-icons/react' ;
4- import { useState } from 'react' ;
4+ import { useMemo , useState } from 'react' ;
55
66export interface JsonNodeProps {
77 data : unknown ;
88 name ?: string ;
99 level ?: number ;
10+ maxDepth ?: number ;
11+ }
12+
13+ const MAX_DEPTH = 20 ;
14+
15+ const indentClasses = [
16+ 'pl-0' ,
17+ 'pl-3' ,
18+ 'pl-6' ,
19+ 'pl-9' ,
20+ 'pl-12' ,
21+ 'pl-[60px]' ,
22+ 'pl-[72px]' ,
23+ 'pl-[84px]' ,
24+ 'pl-[96px]' ,
25+ 'pl-[108px]' ,
26+ 'pl-[120px]' ,
27+ 'pl-[132px]' ,
28+ 'pl-[144px]' ,
29+ 'pl-[156px]' ,
30+ 'pl-[168px]' ,
31+ 'pl-[180px]' ,
32+ 'pl-[192px]' ,
33+ 'pl-[204px]' ,
34+ 'pl-[216px]' ,
35+ 'pl-[228px]' ,
36+ 'pl-[240px]' ,
37+ ] ;
38+
39+ function getKeyColor ( ) {
40+ return 'text-green-600 dark:text-blue-300' ;
1041}
1142
1243function getValueColor ( value : unknown ) {
1344 if ( value === null ) {
1445 return 'text-muted-foreground' ;
1546 }
1647 if ( typeof value === 'string' ) {
17- return 'text-emerald-500 dark:text-emerald -300' ;
48+ return 'text-blue-600 dark:text-orange -300' ;
1849 }
1950 if ( typeof value === 'number' || typeof value === 'boolean' ) {
20- return 'text-amber-500 dark:text-amber -300' ;
51+ return 'text-blue-600 dark:text-orange -300' ;
2152 }
2253 return 'text-foreground/90' ;
2354}
@@ -41,13 +72,12 @@ function PrimitiveNode({
4172 name ?: string ;
4273 level : number ;
4374} ) {
44- const indent = level * 12 ;
75+ const indentClass = indentClasses [ Math . min ( level , indentClasses . length - 1 ) ] ;
4576 return (
4677 < div
47- className = "flex items-center rounded px-2 py-1 transition-colors hover:bg-muted/20"
48- style = { { paddingLeft : indent } }
78+ className = { `flex items-center rounded px-2 py-1 font-mono transition-colors hover:bg-muted/20 ${ indentClass } ` }
4979 >
50- { name && < span className = " mr-2 text-primary" > { name } :</ span > }
80+ { name && < span className = { ` mr-2 ${ getKeyColor ( ) } ` } > { name } :</ span > }
5181 < span className = { getValueColor ( value ) } > { formatValue ( value ) } </ span >
5282 </ div >
5383 ) ;
@@ -57,52 +87,64 @@ function ArrayNode({
5787 data,
5888 name,
5989 level,
90+ maxDepth = MAX_DEPTH ,
6091} : {
6192 data : unknown [ ] ;
6293 name ?: string ;
6394 level : number ;
95+ maxDepth ?: number ;
6496} ) {
6597 const [ isExpanded , setIsExpanded ] = useState ( true ) ;
66- const indent = level * 12 ;
98+ const indentClass = indentClasses [ Math . min ( level , indentClasses . length - 1 ) ] ;
99+
100+ const itemKeys = useMemo (
101+ ( ) => data . map ( ( _ , index ) => `${ name || 'root' } _${ level } _${ index } ` ) ,
102+ [ data , name , level ]
103+ ) ;
104+
67105 if ( data . length === 0 ) {
68106 return < PrimitiveNode level = { level } name = { name } value = "[]" /> ;
69107 }
108+
109+ if ( level >= maxDepth ) {
110+ return (
111+ < PrimitiveNode level = { level } name = { name } value = "[...deeply nested]" />
112+ ) ;
113+ }
114+
70115 return (
71- < div >
116+ < div className = "font-mono" >
72117 < button
73118 aria-expanded = { isExpanded }
74- className = " flex w-full items-center rounded px-2 py-1 text-left transition-colors hover:bg-muted/20"
119+ className = { ` flex w-full items-center rounded px-2 py-1 text-left transition-colors hover:bg-muted/20 ${ indentClass } ` }
75120 onClick = { ( ) => setIsExpanded ( ! isExpanded ) }
76- style = { { paddingLeft : indent } }
77121 type = "button"
78122 >
79123 { isExpanded ? (
80124 < CaretDownIcon className = "mr-1 h-4 w-4 text-muted-foreground" />
81125 ) : (
82126 < CaretRightIcon className = "mr-1 h-4 w-4 text-muted-foreground" />
83127 ) }
84- { name && < span className = " mr-2 text-primary" > { name } :</ span > }
128+ { name && < span className = { ` mr-2 ${ getKeyColor ( ) } ` } > { name } :</ span > }
85129 < span className = "font-semibold text-foreground/80" > [</ span >
86130 </ button >
87131 { isExpanded && (
88132 < >
89133 { data . map ( ( item , index ) => (
90134 < JsonNode
91135 data = { item }
92- key = { ` ${ name || 'root' } - ${ index } ` }
136+ key = { itemKeys [ index ] }
93137 level = { level + 1 }
138+ maxDepth = { maxDepth }
94139 />
95140 ) ) }
96- < div
97- className = "flex items-center py-1"
98- style = { { paddingLeft : indent } }
99- >
141+ < div className = { `flex items-center py-1 ${ indentClass } ` } >
100142 < span className = "font-semibold text-foreground/80" > ]</ span >
101143 </ div >
102144 </ >
103145 ) }
104146 { ! isExpanded && (
105- < div className = " flex items-center py-1" style = { { paddingLeft : indent } } >
147+ < div className = { ` flex items-center py-1 ${ indentClass } ` } >
106148 < span className = "font-semibold text-foreground/80" > ]</ span >
107149 </ div >
108150 ) }
@@ -114,49 +156,70 @@ function ObjectNode({
114156 data,
115157 name,
116158 level,
159+ maxDepth = MAX_DEPTH ,
117160} : {
118161 data : Record < string , unknown > ;
119162 name ?: string ;
120163 level : number ;
164+ maxDepth ?: number ;
121165} ) {
122166 const [ isExpanded , setIsExpanded ] = useState ( true ) ;
123- const indent = level * 12 ;
167+ const indentClass = indentClasses [ Math . min ( level , indentClasses . length - 1 ) ] ;
124168 const keys = Object . keys ( data ) ;
169+
170+ const keyProps = useMemo (
171+ ( ) =>
172+ keys . map ( ( key ) => ( {
173+ key : `${ name || 'root' } _${ level } _${ key } ` ,
174+ name : key ,
175+ } ) ) ,
176+ [ keys , name , level ]
177+ ) ;
178+
125179 if ( keys . length === 0 ) {
126180 return < PrimitiveNode level = { level } name = { name } value = "{}" /> ;
127181 }
182+
183+ if ( level >= maxDepth ) {
184+ return (
185+ < PrimitiveNode level = { level } name = { name } value = "{...deeply nested}" />
186+ ) ;
187+ }
188+
128189 return (
129- < div >
190+ < div className = "font-mono" >
130191 < button
131192 aria-expanded = { isExpanded }
132- className = " flex w-full items-center rounded px-2 py-1 text-left transition-colors hover:bg-muted/20"
193+ className = { ` flex w-full items-center rounded px-2 py-1 text-left transition-colors hover:bg-muted/20 ${ indentClass } ` }
133194 onClick = { ( ) => setIsExpanded ( ! isExpanded ) }
134- style = { { paddingLeft : indent } }
135195 type = "button"
136196 >
137197 { isExpanded ? (
138198 < CaretDownIcon className = "mr-1 h-4 w-4 text-muted-foreground" />
139199 ) : (
140200 < CaretRightIcon className = "mr-1 h-4 w-4 text-muted-foreground" />
141201 ) }
142- { name && < span className = " mr-2 text-primary" > { name } :</ span > }
202+ { name && < span className = { ` mr-2 ${ getKeyColor ( ) } ` } > { name } :</ span > }
143203 < span className = "font-semibold text-foreground/80" > { '{' } </ span >
144204 </ button >
145205 { isExpanded && (
146206 < >
147- { keys . map ( ( key ) => (
148- < JsonNode data = { data [ key ] } key = { key } level = { level + 1 } name = { key } />
207+ { keyProps . map ( ( { key, name : keyName } ) => (
208+ < JsonNode
209+ data = { data [ keyName ] }
210+ key = { key }
211+ level = { level + 1 }
212+ maxDepth = { maxDepth }
213+ name = { keyName }
214+ />
149215 ) ) }
150- < div
151- className = "flex items-center py-1"
152- style = { { paddingLeft : indent } }
153- >
216+ < div className = { `flex items-center py-1 ${ indentClass } ` } >
154217 < span className = "font-semibold text-foreground/80" > { '}' } </ span >
155218 </ div >
156219 </ >
157220 ) }
158221 { ! isExpanded && (
159- < div className = " flex items-center py-1" style = { { paddingLeft : indent } } >
222+ < div className = { ` flex items-center py-1 ${ indentClass } ` } >
160223 < span className = "font-semibold text-foreground/80" > { '}' } </ span >
161224 </ div >
162225 ) }
0 commit comments