@gfazioli/mantine-json-tree 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,9 +27,13 @@ Wrapped with Mantine layout primitives like Paper, Stack, and SimpleGrid, JsonTr
27
27
  ## Features
28
28
 
29
29
  - Interactive collapsible tree view for any JSON-serializable data
30
+ - **Search** with text highlight, filtered tree view, and auto-expand matching branches
31
+ - **Redesigned toolbar** with key count badge, global copy, search toggle, and modern icons
32
+ - **Paper wrapper** with `withBorder` for bordered container look
33
+ - **Custom root name** via `rootName` prop
30
34
  - Syntax highlighting with customizable colors for 16+ data types (strings, numbers, booleans, null, Date, RegExp, Map, Set, BigInt, Symbol, React elements, etc.)
31
35
  - Dark mode support with automatic color adaptation
32
- - Copy-to-clipboard on individual nodes
36
+ - Copy-to-clipboard on individual nodes + global copy all JSON
33
37
  - Keyboard navigation (arrow keys, Space to expand, Ctrl+C to copy)
34
38
  - Configurable expansion depth with expand/collapse all controls
35
39
  - Controlled expand/collapse state with `expanded` and `onExpandedChange` props
@@ -40,10 +44,10 @@ Wrapped with Mantine layout primitives like Paper, Stack, and SimpleGrid, JsonTr
40
44
  - Sticky header support with configurable offset
41
45
  - Function display modes: as-string, hide, or as-object introspection
42
46
  - Responsive font size via Mantine breakpoint objects (CSS-native, no re-renders)
43
- - Full Mantine Styles API support with 13 style selectors and 25+ CSS variables
47
+ - Full Mantine Styles API support with 21 style selectors and 25+ CSS variables
44
48
  - Custom icons for expand/collapse and copy controls
45
49
  - Item count badges for objects and arrays
46
- - `onExpand`, `onCollapse`, `onNodeClick`, and `onCopy` callbacks
50
+ - `onExpand`, `onCollapse`, `onNodeClick`, `onCopy`, and `onCopyAll` callbacks
47
51
 
48
52
  > [!note]
49
53
  >
@@ -4,11 +4,13 @@
4
4
  var React = require('react');
5
5
  var iconsReact = require('@tabler/icons-react');
6
6
  var core = require('@mantine/core');
7
+ var hooks = require('@mantine/hooks');
7
8
  var JsonTreeMediaVariables = require('./JsonTreeMediaVariables.cjs');
8
9
  var utils = require('./lib/utils.cjs');
9
10
  var JsonTree_module = require('./JsonTree.module.css.cjs');
10
11
 
11
12
  const defaultProps = {
13
+ rootName: "root",
12
14
  defaultExpanded: false,
13
15
  maxDepth: 2,
14
16
  withExpandAll: false,
@@ -19,10 +21,71 @@ const defaultProps = {
19
21
  showPathOnHover: false,
20
22
  stickyHeader: false,
21
23
  displayFunctions: "as-string",
22
- expandAllControlIcon: /* @__PURE__ */ React.createElement(iconsReact.IconLibraryPlus, { size: 16 }),
23
- collapseAllControlIcon: /* @__PURE__ */ React.createElement(iconsReact.IconLibraryMinus, { size: 16 }),
24
- copyToClipboardIcon: /* @__PURE__ */ React.createElement(iconsReact.IconCopy, { size: 12 })
24
+ expandAllControlIcon: /* @__PURE__ */ React.createElement(iconsReact.IconArrowBarToDown, { size: 16 }),
25
+ collapseAllControlIcon: /* @__PURE__ */ React.createElement(iconsReact.IconArrowBarToUp, { size: 16 }),
26
+ copyToClipboardIcon: /* @__PURE__ */ React.createElement(iconsReact.IconCopy, { size: 12 }),
27
+ withBorder: false,
28
+ borderRadius: "sm",
29
+ withKeyCountBadge: false,
30
+ withCopyAll: false,
31
+ withSearch: false,
32
+ copyAllIcon: /* @__PURE__ */ React.createElement(iconsReact.IconCopy, { size: 16 }),
33
+ searchIcon: /* @__PURE__ */ React.createElement(iconsReact.IconSearch, { size: 16 }),
34
+ searchPlaceholder: "Filter keys and values...",
35
+ searchDebounce: 300
25
36
  };
37
+ function highlightText(text, query, getStyles) {
38
+ if (!query) {
39
+ return text;
40
+ }
41
+ const lowerText = text.toLowerCase();
42
+ const lowerQuery = query.toLowerCase();
43
+ const idx = lowerText.indexOf(lowerQuery);
44
+ if (idx === -1) {
45
+ return text;
46
+ }
47
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, text.substring(0, idx), /* @__PURE__ */ React.createElement("span", { ...getStyles("searchHighlight") }, text.substring(idx, idx + query.length)), text.substring(idx + query.length));
48
+ }
49
+ function CopyNodeButton({
50
+ icon,
51
+ getStyles,
52
+ onCopy
53
+ }) {
54
+ const [copied, setCopied] = React.useState(false);
55
+ const timeoutRef = React.useRef(null);
56
+ React.useEffect(() => {
57
+ return () => {
58
+ if (timeoutRef.current) {
59
+ clearTimeout(timeoutRef.current);
60
+ }
61
+ };
62
+ }, []);
63
+ const handleClick = async (e) => {
64
+ const success = await onCopy(e);
65
+ if (!success) {
66
+ return;
67
+ }
68
+ setCopied(true);
69
+ if (timeoutRef.current) {
70
+ clearTimeout(timeoutRef.current);
71
+ }
72
+ timeoutRef.current = setTimeout(() => {
73
+ setCopied(false);
74
+ timeoutRef.current = null;
75
+ }, 1500);
76
+ };
77
+ return /* @__PURE__ */ React.createElement(
78
+ core.ActionIcon,
79
+ {
80
+ size: "xs",
81
+ variant: "subtle",
82
+ color: copied ? "green" : "gray",
83
+ onClick: handleClick,
84
+ ...getStyles("copyButton")
85
+ },
86
+ copied ? /* @__PURE__ */ React.createElement(iconsReact.IconCheck, { size: 12 }) : icon
87
+ );
88
+ }
26
89
  function renderJSONNode({ node, expanded, hasChildren, elementProps, tree }, props, ctx, onNodeClick) {
27
90
  const {
28
91
  getStyles,
@@ -62,7 +125,9 @@ function renderJSONNode({ node, expanded, hasChildren, elementProps, tree }, pro
62
125
  const copy = JSON.stringify(value, null, 2);
63
126
  await navigator.clipboard.writeText(copy);
64
127
  onCopy?.(copy, value);
65
- } catch (error) {
128
+ return true;
129
+ } catch {
130
+ return false;
66
131
  }
67
132
  };
68
133
  const handleClick = () => {
@@ -119,26 +184,21 @@ function renderJSONNode({ node, expanded, hasChildren, elementProps, tree }, pro
119
184
  wrap: "nowrap",
120
185
  ...elementProps,
121
186
  onClick: handleClick,
122
- style: { cursor: onNodeClick ? "pointer" : "default", position: "relative" }
187
+ style: {
188
+ cursor: onNodeClick ? "pointer" : "default",
189
+ position: "relative",
190
+ backgroundColor: ctx.directMatches?.has(node.value) ? "rgba(251, 191, 36, 0.15)" : void 0,
191
+ borderRadius: ctx.directMatches?.has(node.value) ? "4px" : void 0
192
+ }
123
193
  },
124
194
  lineNumber,
125
195
  renderIndentGuides(),
126
- key !== void 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(core.Text, { component: "span", ...getStyles("key"), "data-key": key }, key), /* @__PURE__ */ React.createElement(core.Text, { component: "span", ...getStyles("keyValueSeparator") }, ":")),
196
+ key !== void 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(core.Text, { component: "span", ...getStyles("key"), "data-key": key }, ctx.searchQuery ? highlightText(String(key), ctx.searchQuery, getStyles) : key), /* @__PURE__ */ React.createElement(core.Text, { component: "span", ...getStyles("keyValueSeparator") }, ":")),
127
197
  (() => {
128
198
  const formattedValue = utils.formatValue(value, type);
129
- return /* @__PURE__ */ React.createElement(core.Code, { ...getStyles("value"), "data-type": type, "data-value": formattedValue }, formattedValue);
199
+ return /* @__PURE__ */ React.createElement(core.Code, { ...getStyles("value"), "data-type": type, "data-value": formattedValue }, ctx.searchQuery ? highlightText(formattedValue, ctx.searchQuery, getStyles) : formattedValue);
130
200
  })(),
131
- withCopyToClipboard && /* @__PURE__ */ React.createElement(
132
- core.ActionIcon,
133
- {
134
- size: "xs",
135
- variant: "subtle",
136
- color: "gray",
137
- onClick: handleCopy,
138
- ...getStyles("copyButton")
139
- },
140
- copyToClipboardIcon
141
- )
201
+ withCopyToClipboard && /* @__PURE__ */ React.createElement(CopyNodeButton, { icon: copyToClipboardIcon, getStyles, onCopy: handleCopy })
142
202
  )
143
203
  );
144
204
  }
@@ -182,7 +242,12 @@ function renderJSONNode({ node, expanded, hasChildren, elementProps, tree }, pro
182
242
  "data-expanded": expanded,
183
243
  "data-has-children": hasChildren,
184
244
  "data-type": type,
185
- style: { cursor: onNodeClick ? "pointer" : "default", position: "relative" }
245
+ style: {
246
+ cursor: onNodeClick ? "pointer" : "default",
247
+ position: "relative",
248
+ backgroundColor: ctx.directMatches?.has(node.value) ? "rgba(251, 191, 36, 0.15)" : void 0,
249
+ borderRadius: ctx.directMatches?.has(node.value) ? "4px" : void 0
250
+ }
186
251
  },
187
252
  lineNumber,
188
253
  renderIndentGuides(),
@@ -196,7 +261,7 @@ function renderJSONNode({ node, expanded, hasChildren, elementProps, tree }, pro
196
261
  },
197
262
  expandCollapseIcon
198
263
  ),
199
- key !== void 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(core.Text, { component: "span", ...getStyles("key") }, key), /* @__PURE__ */ React.createElement(core.Text, { component: "span", ...getStyles("keyValueSeparator") }, ":")),
264
+ key !== void 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(core.Text, { component: "span", ...getStyles("key") }, ctx.searchQuery ? highlightText(String(key), ctx.searchQuery, getStyles) : key), /* @__PURE__ */ React.createElement(core.Text, { component: "span", ...getStyles("keyValueSeparator") }, ":")),
200
265
  /* @__PURE__ */ React.createElement(core.Text, { component: "span", ...getStyles("bracket") }, openBracket),
201
266
  !expanded && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(core.Text, { component: "span", size: "xs", ...getStyles("ellipsis") }, "..."), /* @__PURE__ */ React.createElement(core.Text, { component: "span", ...getStyles("bracket") }, closeBracket), itemCount !== void 0 && showItemsCount && /* @__PURE__ */ React.createElement(core.Badge, { size: "xs", variant: "light", color: "gray", ...getStyles("itemsCount") }, itemCount)),
202
267
  withCopyToClipboard && /* @__PURE__ */ React.createElement(
@@ -257,7 +322,17 @@ const varsResolver = core.createVarsResolver(
257
322
  lineNumber: { "--json-tree-color-line-number": "var(--mantine-color-gray-5)" },
258
323
  itemsCount: {},
259
324
  controls: {},
260
- copyButton: {}
325
+ copyButton: {},
326
+ paper: {},
327
+ toolbar: {},
328
+ keyCountBadge: {},
329
+ copyAllButton: {},
330
+ searchToggle: {},
331
+ searchBar: {},
332
+ searchInput: {},
333
+ searchHighlight: {
334
+ "--json-tree-search-highlight-color": "var(--mantine-color-yellow-3)"
335
+ }
261
336
  };
262
337
  }
263
338
  );
@@ -265,6 +340,7 @@ const JsonTree = core.factory((_props) => {
265
340
  const props = core.useProps("JsonTree", defaultProps, _props);
266
341
  const {
267
342
  data,
343
+ rootName,
268
344
  defaultExpanded,
269
345
  maxDepth,
270
346
  onNodeClick,
@@ -291,6 +367,19 @@ const JsonTree = core.factory((_props) => {
291
367
  expandControlIcon,
292
368
  collapseControlIcon,
293
369
  size,
370
+ withBorder,
371
+ borderRadius,
372
+ withKeyCountBadge,
373
+ keyCountBadgeLabel,
374
+ withCopyAll,
375
+ copyAllIcon,
376
+ onCopyAll,
377
+ withSearch,
378
+ searchIcon,
379
+ searchPlaceholder,
380
+ searchQuery: controlledSearchQuery,
381
+ onSearchChange,
382
+ searchDebounce,
294
383
  classNames,
295
384
  style,
296
385
  styles,
@@ -313,8 +402,8 @@ const JsonTree = core.factory((_props) => {
313
402
  });
314
403
  const responsiveClassName = core.useRandomClassName();
315
404
  const treeData = React.useMemo(
316
- () => [utils.convertToTreeData(data, void 0, "root", 0, displayFunctions)],
317
- [data, displayFunctions]
405
+ () => [utils.convertToTreeData(data, rootName ?? "root", rootName ?? "root", 0, displayFunctions)],
406
+ [data, rootName, displayFunctions]
318
407
  );
319
408
  const initialExpandedState = React.useMemo(() => {
320
409
  if (controlledExpanded) {
@@ -379,6 +468,83 @@ const JsonTree = core.factory((_props) => {
379
468
  },
380
469
  [withCopyToClipboard, treeData, onCopy]
381
470
  );
471
+ const totalKeyCount = React.useMemo(() => utils.getItemCount(data), [data]);
472
+ const [searchOpen, setSearchOpen] = React.useState(false);
473
+ const [searchQueryInternal, setSearchQueryInternal] = React.useState("");
474
+ const activeSearchQuery = controlledSearchQuery ?? searchQueryInternal ?? "";
475
+ const [debouncedQuery] = hooks.useDebouncedValue(activeSearchQuery, searchDebounce ?? 300);
476
+ const preSearchExpandedRef = React.useRef(null);
477
+ const searchResults = React.useMemo(
478
+ () => utils.searchTree(treeData, debouncedQuery),
479
+ [treeData, debouncedQuery]
480
+ );
481
+ const filteredTreeData = React.useMemo(() => {
482
+ if (!debouncedQuery || searchResults.matchedPaths.size === 0) {
483
+ return treeData;
484
+ }
485
+ return utils.filterTreeBySearch(treeData, searchResults.matchedPaths);
486
+ }, [treeData, debouncedQuery, searchResults]);
487
+ React.useEffect(() => {
488
+ if (debouncedQuery && searchResults.expandedPaths.length > 0) {
489
+ if (!preSearchExpandedRef.current) {
490
+ preSearchExpandedRef.current = { ...tree.expandedState };
491
+ }
492
+ const newState = {};
493
+ searchResults.expandedPaths.forEach((p) => {
494
+ newState[p] = true;
495
+ });
496
+ if (onExpandedChange) {
497
+ onExpandedChange(Object.keys(newState).filter((k) => newState[k]));
498
+ } else {
499
+ tree.setExpandedState(newState);
500
+ }
501
+ }
502
+ }, [debouncedQuery, searchResults]);
503
+ const handleClearSearch = React.useCallback(() => {
504
+ setSearchQueryInternal("");
505
+ onSearchChange?.("");
506
+ if (preSearchExpandedRef.current) {
507
+ if (onExpandedChange) {
508
+ onExpandedChange(
509
+ Object.keys(preSearchExpandedRef.current).filter((k) => preSearchExpandedRef.current[k])
510
+ );
511
+ } else {
512
+ tree.setExpandedState(preSearchExpandedRef.current);
513
+ }
514
+ preSearchExpandedRef.current = null;
515
+ }
516
+ }, [onExpandedChange, onSearchChange, tree]);
517
+ const handleCloseSearch = React.useCallback(() => {
518
+ setSearchOpen(false);
519
+ handleClearSearch();
520
+ }, [handleClearSearch]);
521
+ const handleExpandAll = React.useCallback(() => {
522
+ const allState = core.getTreeExpandedState(treeData, "*");
523
+ if (onExpandedChange) {
524
+ onExpandedChange(Object.keys(allState).filter((k) => allState[k]));
525
+ } else {
526
+ tree.expandAllNodes();
527
+ }
528
+ }, [treeData, onExpandedChange, tree]);
529
+ const handleCollapseAll = React.useCallback(() => {
530
+ if (onExpandedChange) {
531
+ onExpandedChange([]);
532
+ } else {
533
+ tree.collapseAllNodes();
534
+ }
535
+ }, [onExpandedChange, tree]);
536
+ const [copiedAll, setCopiedAll] = React.useState(false);
537
+ const handleCopyAll = React.useCallback(async () => {
538
+ try {
539
+ const json = JSON.stringify(data, null, 2);
540
+ await navigator.clipboard.writeText(json);
541
+ onCopyAll?.(json);
542
+ onCopy?.(json, data);
543
+ setCopiedAll(true);
544
+ setTimeout(() => setCopiedAll(false), 1500);
545
+ } catch {
546
+ }
547
+ }, [data, onCopyAll, onCopy]);
382
548
  const renderCtx = {
383
549
  getStyles,
384
550
  copyToClipboardIcon,
@@ -386,59 +552,99 @@ const JsonTree = core.factory((_props) => {
386
552
  collapseControlIcon,
387
553
  onExpand,
388
554
  onCollapse,
389
- onExpandedChange
555
+ onExpandedChange,
556
+ searchQuery: debouncedQuery || void 0,
557
+ matchedPaths: debouncedQuery ? searchResults.matchedPaths : void 0,
558
+ directMatches: debouncedQuery ? searchResults.directMatches : void 0
390
559
  };
391
560
  const treeComponent = /* @__PURE__ */ React.createElement(
392
561
  core.Tree,
393
562
  {
394
- data: treeData,
563
+ data: filteredTreeData,
395
564
  tree,
396
565
  levelOffset: 32,
397
566
  renderNode: (payload) => renderJSONNode(payload, props, renderCtx, onNodeClick)
398
567
  }
399
568
  );
400
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(JsonTreeMediaVariables.JsonTreeMediaVariables, { size, selector: `.${responsiveClassName}` }), /* @__PURE__ */ React.createElement(
569
+ const showHeader = title || withExpandAll || withKeyCountBadge || withCopyAll || withSearch;
570
+ const content = /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(JsonTreeMediaVariables.JsonTreeMediaVariables, { size, selector: `.${responsiveClassName}` }), /* @__PURE__ */ React.createElement(
401
571
  core.Box,
402
572
  {
403
573
  ...getStyles("root", { className: responsiveClassName }),
404
574
  ...others,
405
575
  "data-line-numbers": showLineNumbers || void 0,
576
+ "data-searching": debouncedQuery ? true : void 0,
406
577
  onKeyDown: handleKeyDown
407
578
  },
408
- (title || withExpandAll) && /* @__PURE__ */ React.createElement(core.Group, { ...getStyles("header"), justify: "space-between", mod: { sticky: stickyHeader } }, title || /* @__PURE__ */ React.createElement("div", null), withExpandAll && utils.isExpandable(data) && /* @__PURE__ */ React.createElement(core.Group, { gap: "xs", style: { top: 10, zIndex: 1 } }, /* @__PURE__ */ React.createElement(
579
+ showHeader && /* @__PURE__ */ React.createElement(core.Group, { ...getStyles("header"), justify: "space-between", mod: { sticky: stickyHeader } }, /* @__PURE__ */ React.createElement(core.Group, { gap: "xs" }, title || /* @__PURE__ */ React.createElement("div", null), withKeyCountBadge && utils.isExpandable(data) && /* @__PURE__ */ React.createElement(core.Badge, { size: "sm", variant: "light", color: "gray", ...getStyles("keyCountBadge") }, keyCountBadgeLabel ? keyCountBadgeLabel(totalKeyCount) : `${totalKeyCount} ${Array.isArray(data) ? "items" : "keys"}`)), /* @__PURE__ */ React.createElement(core.Group, { gap: 4, ...getStyles("toolbar") }, withSearch && /* @__PURE__ */ React.createElement(
409
580
  core.ActionIcon,
410
581
  {
411
- size: "xs",
412
- variant: "transparent",
582
+ size: "sm",
583
+ variant: searchOpen ? "light" : "subtle",
584
+ color: "gray",
413
585
  onClick: () => {
414
- const allState = core.getTreeExpandedState(treeData, "*");
415
- if (onExpandedChange) {
416
- onExpandedChange(Object.keys(allState).filter((k) => allState[k]));
586
+ if (searchOpen) {
587
+ handleCloseSearch();
417
588
  } else {
418
- tree.expandAllNodes();
589
+ setSearchOpen(true);
419
590
  }
420
591
  },
592
+ ...getStyles("searchToggle")
593
+ },
594
+ searchIcon
595
+ ), withExpandAll && utils.isExpandable(data) && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
596
+ core.ActionIcon,
597
+ {
598
+ size: "sm",
599
+ variant: "subtle",
600
+ color: "gray",
601
+ onClick: handleExpandAll,
421
602
  ...getStyles("controls")
422
603
  },
423
604
  expandAllControlIcon
424
605
  ), /* @__PURE__ */ React.createElement(
425
606
  core.ActionIcon,
426
607
  {
427
- size: "xs",
428
- variant: "transparent",
429
- onClick: () => {
430
- if (onExpandedChange) {
431
- onExpandedChange([]);
432
- } else {
433
- tree.collapseAllNodes();
434
- }
435
- },
608
+ size: "sm",
609
+ variant: "subtle",
610
+ color: "gray",
611
+ onClick: handleCollapseAll,
436
612
  ...getStyles("controls")
437
613
  },
438
614
  collapseAllControlIcon
615
+ )), withCopyAll && /* @__PURE__ */ React.createElement(
616
+ core.ActionIcon,
617
+ {
618
+ size: "sm",
619
+ variant: "subtle",
620
+ color: copiedAll ? "green" : "gray",
621
+ onClick: handleCopyAll,
622
+ ...getStyles("copyAllButton")
623
+ },
624
+ copiedAll ? /* @__PURE__ */ React.createElement(iconsReact.IconCheck, { size: 16 }) : copyAllIcon
625
+ ))),
626
+ searchOpen && withSearch && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(core.Divider, null), /* @__PURE__ */ React.createElement(core.Box, { ...getStyles("searchBar"), p: "xs" }, /* @__PURE__ */ React.createElement(
627
+ core.TextInput,
628
+ {
629
+ placeholder: searchPlaceholder,
630
+ value: activeSearchQuery,
631
+ onChange: (e) => {
632
+ const val = e.currentTarget.value;
633
+ setSearchQueryInternal(val);
634
+ onSearchChange?.(val);
635
+ },
636
+ leftSection: /* @__PURE__ */ React.createElement(iconsReact.IconSearch, { size: 14 }),
637
+ rightSection: activeSearchQuery ? /* @__PURE__ */ React.createElement(core.CloseButton, { size: "sm", onClick: handleClearSearch }) : null,
638
+ size: "sm",
639
+ ...getStyles("searchInput")
640
+ }
439
641
  ))),
440
642
  maxHeight ? /* @__PURE__ */ React.createElement(core.ScrollArea.Autosize, { mah: maxHeight }, treeComponent) : treeComponent
441
643
  ));
644
+ if (withBorder) {
645
+ return /* @__PURE__ */ React.createElement(core.Paper, { withBorder: true, radius: borderRadius, ...getStyles("paper") }, content);
646
+ }
647
+ return content;
442
648
  });
443
649
  JsonTree.classes = JsonTree_module;
444
650
  JsonTree.displayName = "JsonTree";