├── .gitignore ├── .npmignore ├── .prettierignore ├── .storybook ├── main.ts ├── preview.ts ├── public │ ├── favicon.ico │ └── favicon.svg └── style.css ├── LICENSE ├── README.md ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── postcss.config.js ├── prettier.config.cjs ├── rollup.config.js ├── sample.png ├── src ├── components │ ├── copy-button.tsx │ ├── json-node.tsx │ ├── json-view.tsx │ ├── large-array-node.tsx │ ├── large-array.tsx │ ├── long-string.tsx │ ├── name-value.tsx │ └── object-node.tsx ├── dark.css ├── global.d.ts ├── index.tsx ├── stories │ ├── editable.stories.tsx │ ├── json-view.stories.tsx │ ├── share.ts │ └── theme.stories.tsx ├── style.css ├── svgs │ ├── add-circle.svg │ ├── add-square.svg │ ├── angle-down.svg │ ├── cancel.svg │ ├── copied.svg │ ├── copy.svg │ ├── custom-add.svg │ ├── custom-copied.svg │ ├── custom-copy.svg │ ├── done.svg │ ├── edit.svg │ ├── link.svg │ └── trash.svg ├── types.ts └── utils.ts ├── tailwind.config.cjs ├── theme.png ├── tsconfig.json ├── turbo.json ├── vercel.json └── website ├── .gitignore ├── .prettierignore ├── README.md ├── global.d.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public ├── favicon.svg └── og.png ├── src ├── app │ ├── footer.tsx │ ├── globals.css │ ├── head.tsx │ ├── layout.tsx │ └── page.tsx ├── components │ ├── single-loading.tsx │ └── theme.tsx ├── contents │ ├── collapse-string.tsx │ ├── collapsed.tsx │ ├── customization.tsx │ ├── display-array-index.tsx │ ├── display-size.tsx │ ├── editable.tsx │ ├── hero.tsx │ ├── installation.tsx │ ├── themes.tsx │ └── usage.tsx ├── hooks │ └── useTheme.ts ├── lib │ ├── clipboard.ts │ ├── hljs.ts │ └── storage.ts └── svgs │ ├── angle-down.svg │ ├── copied.svg │ ├── copy.svg │ ├── dark.svg │ ├── github.svg │ ├── light.svg │ └── system.svg ├── tailwind.config.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | # Editor directories and files 5 | .vscode/* 6 | !.vscode/extensions.json 7 | .idea 8 | .DS_Store 9 | *.suo 10 | *.ntvs* 11 | *.njsproj 12 | *.sln 13 | *.sw? 14 | 15 | # Storybook 16 | storybook-static 17 | 18 | .turbo -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sample.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | README.md 4 | .next 5 | pnpm-lock.yaml 6 | dist 7 | storybook-static -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite' 2 | import { mergeConfig } from 'vite' 3 | import svgr from 'vite-plugin-svgr' 4 | 5 | const config: StorybookConfig = { 6 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 7 | addons: [ 8 | '@storybook/addon-links', 9 | '@storybook/addon-essentials', 10 | '@storybook/addon-interactions', 11 | { 12 | name: '@storybook/addon-styling', 13 | options: { 14 | postCss: true 15 | } 16 | } 17 | ], 18 | framework: { 19 | name: '@storybook/react-vite', 20 | options: {} 21 | }, 22 | docs: { 23 | autodocs: true 24 | }, 25 | viteFinal(config) { 26 | return mergeConfig(config, { 27 | plugins: [svgr()] 28 | }) 29 | }, 30 | staticDirs: ['./public'], 31 | managerHead: head => ` 32 | ${head} 33 | 34 | ` 35 | } 36 | export default config 37 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react' 2 | import '../src/style.css' 3 | import './style.css' 4 | 5 | const preview: Preview = { 6 | parameters: { 7 | actions: { argTypesRegex: '^on[A-Z].*' }, 8 | controls: { 9 | matchers: { 10 | color: /(background|color)$/i, 11 | date: /Date$/ 12 | } 13 | } 14 | } 15 | } 16 | 17 | export default preview 18 | -------------------------------------------------------------------------------- /.storybook/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYsuni/react18-json-view/b8513baf7ee215ee6105aa05a6c6c71c6515ee4e/.storybook/public/favicon.ico -------------------------------------------------------------------------------- /.storybook/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.storybook/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | #storybook-root { 8 | @apply h-full; 9 | } 10 | body { 11 | padding: 0 !important; 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Suni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [React18 JSON View](https://jv.yysuni.com/) 2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

14 | 15 | React function component for displaying javascript arrays and JSON objects. Supports all JS types. 16 | 17 | [Website](https://jv.yysuni.com/), [Storybook](https://react18-json-view.vercel.app/),[Online](https://json-view-online.vercel.app/) 18 | 19 | ![JSON View](sample.png 'JSON View') 20 | 21 | ## Installation 22 | 23 | ```bash 24 | npm i react18-json-view 25 | ``` 26 | 27 | ```bash 28 | npm i react18-json-view@canary 29 | ``` 30 | 31 | ## Usage 32 | 33 | ```tsx 34 | import JsonView from 'react18-json-view' 35 | import 'react18-json-view/src/style.css' 36 | // If dark mode is needed, import `dark.css`. 37 | // import 'react18-json-view/src/dark.css' 38 | 39 | 40 | 41 | // If needed, you can use the internal stringify function. 42 | // import { stringify } from 'react18-json-view' 43 | ``` 44 | 45 | ### Props 46 | 47 | | Name | Type | Default | Description | 48 | | :------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 49 | | `src` | `JSON Object` | None |
This property contains your input JSON
| 50 | | `className` | `string` | None | The CSS class name(s) to apply to the component. | 51 | | `style` | `JSON Object` | None | An object containing custom style rules to apply to the component. | 52 | | `dark` | `boolean` | `false` | Keep in dark mode (Don't forget to import `dark.css`) | 53 | | `theme` | `default` \| `a11y` \| `github` \| `vscode` \| `atom`\|`winter-is-coming` | `'default'` | Color theme | 54 | | `enableClipboard` | `boolean` | `true` | Whether enable clipboard feature. | 55 | | `matchesURL` | `boolean` | `true` | Show the link icon if value is string and matches URL regex pattern. | 56 | | `urlRegExp` | `RegExp` | `/^(((ht\|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/` | URL RegExp pattern. | 57 | | `displaySize` | `boolean` \| `integer` \| 'collapsed' \| 'expanded' | `false` | Whether display the size of `Object`, `Array`. | 58 | | `displayArrayIndex` | `boolean` | `true` | Whether display the index of `Array`. | 59 | | `collapseStringsAfterLength` | `integer` | `99` | When an integer value is assigned, strings longer than that length will be truncated and indicated by an ellipsis. To expand or collapse the string content, simply click on the string value. | 60 | | `customizeCollapseStringUI` | ` (str_show: string, truncated: boolean) => (JSX.Element \| string)` \| `string` | - | Customize the collapse string UI. | 61 | | `ignoreLargeArray` | `boolean` | `false` | Prevent collapsing large array(length > 100) behavior since v0.2.7 | 62 | | `collapseStringMode` | `'directly'` \| `'word'` \| `'address'` | `'directly'` | If the `word` is assigned, the collapsed length will be adjusted to fully display the last word. | 63 | | `collapsed` | `boolean` \| `integer` \| `function` | `false` | When set to true(false), all nodes will be (not) collapsed by default. When using an integer value, it will collapse at a specific depth. The collapsed also can be a function. | 64 | | `onCollapse` | `function` | - | `(params: { isCollapsing: boolean, node: Record \| Array, indexOrName: string \| number \| undefined, depth: number }) => void` | 65 | | `collapseObjectsAfterLength` | `integer` | `99` | When an integer value is assigned, the object and array will initially collapse. | 66 | | `editable` | `boolean` \| {add?: `boolean`, edit?: `boolean`, delete?: `boolean`} | `false` | When set to true, you can add, edit, or delete the property, and the actions will trigger onAdd, onEdit, or onDelete. Options is available. | 67 | | `onAdd` | `function` | - | `(params: { indexOrName: string\| number, depth: number, src: any; parentType: 'object' \| 'array' }) => void` | 68 | | `onDelete` | `function` | - | `(params:{ value: any,indexOrName: string \| number,depth: number,src: any,parentType: 'object' \| 'array'}) => void` | 69 | | `onEdit` | `function` | - | `(params: { newValue: any, oldValue: any, depth: number, src: any, indexOrName: string \| number, parentType: 'object' \| 'array'}) => void` | 70 | | `customizeNode` | `ReactElement`\|`ReactComponent`\|`Options` | - | Highly customize every node. | 71 | | `customizeCopy` | `(node: any, nodeMeta: NodeMeta) => any` | internal stringify | Customize copy behavior, only the returned non-empty string will be written to clipboard. | 72 | | `CopyComponent` \/ `DoneComponent` \/ `CancelComponent` | `React.FC` \/ `React.Component` `<{ onClick: (event: React.MouseEvent) => void; className: string ; style: React.CSSProperties}>` | - | Customize copy icon. | 73 | | `CopiedComponent` | `React.FC` \/ `React.Component` `<{ className: string; style: React.CSSProperties }>` | - | Customize copied icon. | 74 | | `CustomOperation` | `React.FC` \/ `React.Component` `<{ node: any }>` | - | Custom Operation | 75 | 76 | ### Collapsed function 77 | 78 | ```ts 79 | ;(params: { 80 | node: Record | Array // Object or array 81 | indexOrName: number | string | undefined 82 | depth: number 83 | size: number // Object's size or array's length 84 | }) => boolean 85 | ``` 86 | 87 | ### Editable options 88 | 89 | ```ts 90 | { 91 | add?: boolean 92 | edit?: boolean 93 | delete?: boolean 94 | } 95 | ``` 96 | 97 | > onEdit, OnDelete, onAdd and OnChange add `parentPath` field, allow us to correctly indicate which field in the JSON is being edited. 98 | 99 | ### CustomizeNode 100 | 101 | ```ts 102 | (params: { node: any; indexOrName: number | string | undefined; depth: number }) => 103 | | { 104 | add?: boolean 105 | edit?: boolean 106 | delete?: boolean 107 | enableClipboard?: boolean 108 | collapsed?: boolean 109 | className?: string 110 | } 111 | | React.FC 112 | | typeof React.Component 113 | | React.ReactElement 114 | ``` 115 | 116 | ### NodeMeta 117 | 118 | ```ts 119 | { depth: number; indexOrName?: number | string; parent?: Record | Array; parentPath: string[], currentPath: string[] } 120 | ``` 121 | 122 | ## Editable 123 | 124 | ### How to generate object/array 125 | 126 | The editor uses `JSON.parse()`. While in edit mode, you can enter `({})` or `([])`, which will cause the result of eval to become a new object or array. 127 | 128 | > `{}` and `[]` will be auto convert to `({})`,`([])` 129 | 130 | ### How the editor works 131 | 132 | This component does not perform any cloning operations, so every step of the operation is carried out on the original object. If cloning is required, please handle it yourself. 133 | 134 | ### Edit keyboard shortcuts 135 | 136 | When element is editable: 137 | 138 | - `Ctrl/Cmd+Click` => Edit Mode 139 | - `Enter` => Submit 140 | - `Esc` => Cancel 141 | 142 | ## Custom themes 143 | 144 | Below are the default theme variables that you can easily customize to fit your needs. 145 | 146 | ```css 147 | .json-view { 148 | color: #4d4d4d; 149 | --json-property: #009033; 150 | --json-index: #676dff; 151 | --json-number: #676dff; 152 | --json-string: #b2762e; 153 | --json-boolean: #dc155e; 154 | --json-null: #dc155e; 155 | } 156 | .json-view .json-view--property { 157 | color: var(--json-property); 158 | } 159 | .json-view .json-view--index { 160 | color: var(--json-index); 161 | } 162 | .json-view .json-view--number { 163 | color: var(--json-number); 164 | } 165 | .json-view .json-view--string { 166 | color: var(--json-string); 167 | } 168 | .json-view .json-view--boolean { 169 | color: var(--json-boolean); 170 | } 171 | .json-view .json-view--null { 172 | color: var(--json-null); 173 | } 174 | ``` 175 | 176 | ## Why 177 | 178 | react-json-view does not support React 18. 179 | 180 | ## Todo 181 | 182 | - [x] copy (enableClipboard) 183 | - [x] css 184 | - [x] collapse at a particular depth (collapsed) 185 | - [x] editable 186 | - [x] add 187 | - [x] edit 188 | - [x] delete 189 | - [x] onChange 190 | - [ ] onSelect 191 | - [x] dark mode 192 | - [x] custom icon 193 | - [x] export default icons 194 | - [x] more usability/scenarios 195 | - [ ] gif guide 196 | - [x] more color themes(dark) 197 | - [x] collapse objects callback 198 | - [x] editable option 199 | - [x] advance customization 200 | - [ ] access internal actions 201 | - [ ] map/set viewer 202 | - [ ] display data type 203 | - [x] display object size 204 | - [ ] handle circle loop 205 | - [x] redesign docs 206 | - [x] truncate long strings 207 | - [x] custom `stringify` 208 | - [x] split large array 209 | 210 | * tree? 211 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react18-json-view", 3 | "version": "0.2.9", 4 | "type": "module", 5 | "description": "JSON viewer for react18", 6 | "main": "dist/cjs/index.cjs", 7 | "types": "dist/index", 8 | "module": "dist/es/index.mjs", 9 | "brower": "dist/es/index.mjs", 10 | "scripts": { 11 | "dev": "rollup -cw", 12 | "dev:website": "turbo run dev --filter=website...", 13 | "build": "rollup -c", 14 | "storybook": "storybook dev -p 6006", 15 | "build-storybook": "storybook build", 16 | "prepare": "husky install", 17 | "clean": "rm -rf dist", 18 | "format": "prettier --write .", 19 | "prebuild": "npm run clean", 20 | "prepublishOnly": "npm run build" 21 | }, 22 | "files": [ 23 | "src", 24 | "dist" 25 | ], 26 | "devDependencies": { 27 | "@rollup/plugin-commonjs": "^24.0.0", 28 | "@rollup/plugin-node-resolve": "^15.0.1", 29 | "@rollup/plugin-url": "^8.0.1", 30 | "@storybook/addon-essentials": "^7.3.2", 31 | "@storybook/addon-interactions": "^7.3.2", 32 | "@storybook/addon-links": "^7.3.2", 33 | "@storybook/addon-styling": "^1.3.6", 34 | "@storybook/blocks": "^7.3.2", 35 | "@storybook/react": "^7.3.2", 36 | "@storybook/react-vite": "^7.3.2", 37 | "@svgr/rollup": "^8.1.0", 38 | "@types/node": "^18.11.18", 39 | "@types/react": "^18.0.26", 40 | "@types/react-dom": "^18.0.8", 41 | "autoprefixer": "^10.4.14", 42 | "husky": "^8.0.3", 43 | "lint-staged": "^13.1.0", 44 | "postcss": "^8.4.23", 45 | "postcss-loader": "^7.2.4", 46 | "prettier": "^2.8.2", 47 | "prettier-plugin-tailwindcss": "^0.2.8", 48 | "react": "^18.2.0", 49 | "rollup": "^3.9.1", 50 | "rollup-plugin-peer-deps-external": "^2.2.4", 51 | "rollup-plugin-typescript2": "^0.34.1", 52 | "storybook": "^7.3.2", 53 | "tailwindcss": "^3.3.2", 54 | "tslib": "^2.7.0", 55 | "turbo": "^1.10.13", 56 | "typescript": "^5.0.4", 57 | "vite-plugin-svgr": "^3.2.0" 58 | }, 59 | "peerDependencies": { 60 | "react": ">=16.8.0" 61 | }, 62 | "lint-staged": { 63 | "**/*": "prettier --write --ignore-unknown" 64 | }, 65 | "license": "MIT", 66 | "author": "Suni", 67 | "keywords": [ 68 | "function component", 69 | "interactive", 70 | "interactive-json", 71 | "json", 72 | "json-component", 73 | "json-display", 74 | "json-tree", 75 | "json-view", 76 | "json-viewer", 77 | "json-inspector", 78 | "json-tree", 79 | "react", 80 | "react18", 81 | "react-component", 82 | "react-json", 83 | "theme", 84 | "tree", 85 | "tree-view", 86 | "treeview" 87 | ], 88 | "homepage": "https://github.com/YYsuni/react18-json-view#readme", 89 | "repository": { 90 | "type": "git", 91 | "url": "git+https://github.com/YYsuni/react18-json-view.git" 92 | }, 93 | "bugs": { 94 | "url": "https://github.com/YYsuni/react18-json-view/issues" 95 | }, 96 | "sideEffects": [ 97 | "*.css" 98 | ], 99 | "dependencies": { 100 | "copy-to-clipboard": "^3.3.3" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'website' 3 | - '.' 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | jsxSingleQuote: true, 3 | jsxBracketSameLine: true, 4 | printWidth: 160, 5 | singleQuote: true, 6 | trailingComma: 'none', 7 | useTabs: true, 8 | tabWidth: 2, 9 | semi: false, 10 | arrowParens: 'avoid', 11 | bracketSameLine: true 12 | } 13 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import { nodeResolve } from '@rollup/plugin-node-resolve' 3 | import commonjs from '@rollup/plugin-commonjs' 4 | import external from 'rollup-plugin-peer-deps-external' 5 | import svgr from '@svgr/rollup' 6 | import url from '@rollup/plugin-url' 7 | 8 | const plugins = [ 9 | external(), 10 | url(), 11 | svgr({ 12 | svgoConfig: { 13 | plugins: [ 14 | { 15 | name: 'preset-default', 16 | params: { 17 | overrides: { 18 | removeViewBox: false 19 | } 20 | } 21 | } 22 | ] 23 | } 24 | }), 25 | typescript({ useTsconfigDeclarationDir: true }), 26 | nodeResolve(), 27 | commonjs() 28 | ] 29 | 30 | /** @type {import('rollup').InputOption} */ 31 | export default { 32 | input: ['src/index.tsx'], 33 | output: [ 34 | { 35 | dir: 'dist/cjs', 36 | format: 'cjs', 37 | sourcemap: true, 38 | entryFileNames: '[name].cjs', 39 | chunkFileNames: '[name]-[hash].cjs' 40 | }, 41 | { 42 | dir: 'dist/es', 43 | format: 'es', 44 | sourcemap: true, 45 | entryFileNames: '[name].mjs', 46 | chunkFileNames: '[name]-[hash].mjs' 47 | } 48 | ], 49 | plugins 50 | } 51 | -------------------------------------------------------------------------------- /sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYsuni/react18-json-view/b8513baf7ee215ee6105aa05a6c6c71c6515ee4e/sample.png -------------------------------------------------------------------------------- /src/components/copy-button.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from 'react' 2 | import { ReactComponent as CopySVG } from '../svgs/copy.svg' 3 | import { ReactComponent as CopiedSVG } from '../svgs/copied.svg' 4 | import { JsonViewContext } from './json-view' 5 | import { writeClipboard } from 'src/utils' 6 | import { NodeMeta } from 'src/types' 7 | 8 | interface Props { 9 | node: any 10 | nodeMeta: NodeMeta 11 | } 12 | 13 | export default function CopyButton({ node, nodeMeta }: Props) { 14 | const { customizeCopy, CopyComponent, CopiedComponent } = useContext(JsonViewContext) 15 | 16 | const [copied, setCopied] = useState(false) 17 | 18 | const copyHandler = (event: React.MouseEvent) => { 19 | event.stopPropagation() 20 | 21 | const value = customizeCopy(node, nodeMeta) 22 | 23 | if (typeof value === 'string' && value) { 24 | writeClipboard(value) 25 | } 26 | 27 | setCopied(true) 28 | setTimeout(() => setCopied(false), 3000) 29 | } 30 | 31 | return copied ? ( 32 | typeof CopiedComponent === 'function' ? ( 33 | 34 | ) : ( 35 | 36 | ) 37 | ) : typeof CopyComponent === 'function' ? ( 38 | 39 | ) : ( 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/components/json-node.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useRef, useState, isValidElement, useMemo, useCallback } from 'react' 2 | import { JsonViewContext } from './json-view' 3 | import { 4 | customCopy, 5 | customDelete, 6 | customEdit, 7 | editableDelete, 8 | editableEdit, 9 | isObject, 10 | isReactComponent, 11 | safeCall, 12 | stringifyForCopying, 13 | resolveEvalFailedNewValue, 14 | customMatchesURL 15 | } from '../utils' 16 | import ObjectNode from './object-node' 17 | import LongString from './long-string' 18 | import CopyButton from './copy-button' 19 | import { ReactComponent as EditSVG } from '../svgs/edit.svg' 20 | import { ReactComponent as DeleteSVG } from '../svgs/trash.svg' 21 | import { ReactComponent as DoneSVG } from '../svgs/done.svg' 22 | import { ReactComponent as CancelSVG } from '../svgs/cancel.svg' 23 | import { ReactComponent as LinkSVG } from '../svgs/link.svg' 24 | import type { CustomizeNode, CustomizeOptions } from '../types' 25 | 26 | interface Props { 27 | node: any 28 | depth: number 29 | deleteHandle?: (indexOrName: string | number, parentPath: string[]) => void 30 | editHandle?: (indexOrName: string | number, newValue: any, oldValue: any, parentPath: string[]) => void 31 | indexOrName?: number | string 32 | parent?: Record | Array 33 | parentPath: string[] 34 | } 35 | 36 | export default function JsonNode({ node, depth, deleteHandle: _deleteHandle, indexOrName, parent, editHandle, parentPath }: Props) { 37 | // prettier-ignore 38 | const { collapseStringsAfterLength, enableClipboard, editable, src, onDelete, onChange, customizeNode, matchesURL, urlRegExp, EditComponent, DoneComponent, CancelComponent, CustomOperation } = useContext(JsonViewContext) 39 | 40 | let customReturn: ReturnType | undefined 41 | if (typeof customizeNode === 'function') customReturn = safeCall(customizeNode, [{ node, depth, indexOrName }]) 42 | 43 | if (customReturn) { 44 | if (isValidElement(customReturn)) return customReturn 45 | else if (isReactComponent(customReturn)) { 46 | const CustomComponent = customReturn 47 | return 48 | } 49 | } 50 | 51 | if (Array.isArray(node) || isObject(node)) { 52 | return ( 53 | 62 | ) 63 | } else { 64 | const type = typeof node 65 | const currentPath = typeof indexOrName !== 'undefined' ? [...parentPath, String(indexOrName)] : parentPath 66 | 67 | const [editing, setEditing] = useState(false) 68 | const [deleting, setDeleting] = useState(false) 69 | const valueRef = useRef(null) 70 | 71 | const edit = () => { 72 | setEditing(true) 73 | setTimeout(() => { 74 | window.getSelection()?.selectAllChildren(valueRef.current!) 75 | valueRef.current?.focus() 76 | }) 77 | } 78 | 79 | const done = useCallback(() => { 80 | let newValue = valueRef.current!.innerText 81 | 82 | try { 83 | const parsedValue = JSON.parse(newValue) 84 | 85 | if (editHandle) editHandle(indexOrName!, parsedValue, node, parentPath) 86 | } catch (e) { 87 | const trimmedStringValue = resolveEvalFailedNewValue(type, newValue) 88 | if (editHandle) editHandle(indexOrName!, trimmedStringValue, node, parentPath) 89 | } 90 | 91 | setEditing(false) 92 | }, [editHandle, indexOrName, node, parentPath, type]) 93 | const cancel = () => { 94 | setEditing(false) 95 | setDeleting(false) 96 | } 97 | const deleteHandle = () => { 98 | setDeleting(false) 99 | if (_deleteHandle) _deleteHandle(indexOrName!, parentPath) 100 | if (onDelete) 101 | onDelete({ 102 | value: node, 103 | depth, 104 | src, 105 | indexOrName: indexOrName!, 106 | parentType: Array.isArray(parent) ? 'array' : 'object', 107 | parentPath 108 | }) 109 | if (onChange) 110 | onChange({ 111 | depth, 112 | src, 113 | indexOrName: indexOrName!, 114 | parentType: Array.isArray(parent) ? 'array' : 'object', 115 | type: 'delete', 116 | parentPath 117 | }) 118 | } 119 | 120 | const handleKeyDown = useCallback( 121 | (event: React.KeyboardEvent) => { 122 | if (event.key === 'Enter') { 123 | event.preventDefault() 124 | done() 125 | } else if (event.key === 'Escape') { 126 | cancel() 127 | } 128 | }, 129 | [done] 130 | ) 131 | 132 | const isEditing = editing || deleting 133 | const ctrlClick = 134 | !isEditing && editableEdit(editable) && customEdit(customReturn as CustomizeOptions | undefined) && editHandle 135 | ? (event: React.MouseEvent) => { 136 | if (event.ctrlKey || event.metaKey) edit() 137 | } 138 | : undefined 139 | const Icons = ( 140 | <> 141 | {isEditing && 142 | (typeof DoneComponent === 'function' ? ( 143 | 144 | ) : ( 145 | 146 | ))} 147 | {isEditing && 148 | (typeof CancelComponent === 'function' ? ( 149 | 150 | ) : ( 151 | 152 | ))} 153 | 154 | {!isEditing && enableClipboard && customCopy(customReturn as CustomizeOptions | undefined) && ( 155 | 156 | )} 157 | {!isEditing && matchesURL && type === 'string' && urlRegExp.test(node) && customMatchesURL(customReturn as CustomizeOptions | undefined) && ( 158 | 159 | 160 | 161 | )} 162 | 163 | {!isEditing && 164 | editableEdit(editable) && 165 | customEdit(customReturn as CustomizeOptions | undefined) && 166 | editHandle && 167 | (typeof EditComponent === 'function' ? ( 168 | 169 | ) : ( 170 | 171 | ))} 172 | {!isEditing && editableDelete(editable) && customDelete(customReturn as CustomizeOptions | undefined) && _deleteHandle && ( 173 | setDeleting(true)} /> 174 | )} 175 | {typeof CustomOperation === 'function' ? : null} 176 | 177 | ) 178 | 179 | let className = 'json-view--string' 180 | 181 | switch (type) { 182 | case 'number': 183 | case 'bigint': 184 | className = 'json-view--number' 185 | break 186 | case 'boolean': 187 | className = 'json-view--boolean' 188 | break 189 | case 'object': 190 | className = 'json-view--null' 191 | break 192 | } 193 | 194 | if (typeof (customReturn as CustomizeOptions)?.className === 'string') className += ' ' + (customReturn as CustomizeOptions).className 195 | 196 | if (deleting) className += ' json-view--deleting' 197 | 198 | let displayValue = String(node) 199 | if (type === 'bigint') displayValue += 'n' 200 | 201 | const EditingElement = useMemo( 202 | () => ( 203 | 210 | ), 211 | [displayValue, type, handleKeyDown] 212 | ) 213 | 214 | if (type === 'string') 215 | return ( 216 | <> 217 | {editing ? ( 218 | EditingElement 219 | ) : node.length > collapseStringsAfterLength ? ( 220 | 221 | ) : ( 222 | 223 | "{displayValue}" 224 | 225 | )} 226 | 227 | {Icons} 228 | 229 | ) 230 | else { 231 | return ( 232 | <> 233 | {editing ? ( 234 | EditingElement 235 | ) : ( 236 | 237 | {displayValue} 238 | 239 | )} 240 | 241 | {Icons} 242 | 243 | ) 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/components/json-view.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, createContext, useCallback, useEffect, useState } from 'react' 2 | import JsonNode from './json-node' 3 | import type { Collapsed, CustomizeCollapseStringUI, CustomizeNode, DisplaySize, Editable, NodeMeta } from '../types' 4 | import { stringifyForCopying } from '../utils' 5 | 6 | type OnEdit = (params: { 7 | newValue: any 8 | oldValue: any 9 | depth: number 10 | src: any 11 | indexOrName: string | number 12 | parentType: 'object' | 'array' | null 13 | parentPath: string[] 14 | }) => void 15 | type OnDelete = (params: { 16 | value: any 17 | indexOrName: string | number 18 | depth: number 19 | src: any 20 | parentType: 'object' | 'array' | null 21 | parentPath: string[] 22 | }) => void 23 | type OnAdd = (params: { indexOrName: string | number; depth: number; src: any; parentType: 'object' | 'array'; parentPath: string[] }) => void 24 | type OnChange = (params: { 25 | indexOrName: string | number 26 | depth: number 27 | src: any 28 | parentType: 'object' | 'array' | null 29 | type: 'add' | 'edit' | 'delete' 30 | parentPath: string[] 31 | }) => void 32 | type OnCollapse = (params: { isCollapsing: boolean; node: Record | Array; indexOrName: string | number | undefined; depth: number }) => void 33 | 34 | export const defaultURLRegExp = /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/ 35 | 36 | export const JsonViewContext = createContext({ 37 | src: undefined as any, 38 | 39 | collapseStringsAfterLength: 99, 40 | collapseStringMode: 'directly' as 'directly' | 'word' | 'address', 41 | customizeCollapseStringUI: undefined as CustomizeCollapseStringUI | undefined, 42 | 43 | collapseObjectsAfterLength: 20, 44 | collapsed: false as Collapsed, 45 | onCollapse: undefined as OnCollapse | undefined, 46 | enableClipboard: true, 47 | 48 | editable: false as Editable, 49 | onEdit: undefined as OnEdit | undefined, 50 | onDelete: undefined as OnDelete | undefined, 51 | onAdd: undefined as OnAdd | undefined, 52 | onChange: undefined as OnChange | undefined, 53 | 54 | forceUpdate: () => {}, 55 | 56 | customizeNode: undefined as CustomizeNode | undefined, 57 | customizeCopy: (() => {}) as (node: any, nodeMeta?: NodeMeta) => any, 58 | 59 | displaySize: undefined as DisplaySize, 60 | displayArrayIndex: true, 61 | 62 | matchesURL: false, 63 | urlRegExp: defaultURLRegExp, 64 | 65 | ignoreLargeArray: false, 66 | 67 | CopyComponent: undefined as 68 | | React.FC<{ onClick: (event: React.MouseEvent) => void; className: string }> 69 | | React.Component<{ onClick: (event: React.MouseEvent) => void; className: string }> 70 | | undefined, 71 | CopiedComponent: undefined as 72 | | React.FC<{ className: string; style: React.CSSProperties }> 73 | | React.Component<{ className: string; style: React.CSSProperties }> 74 | | undefined, 75 | EditComponent: undefined as 76 | | React.FC<{ onClick: (event: React.MouseEvent) => void; className: string }> 77 | | React.Component<{ onClick: (event: React.MouseEvent) => void; className: string }> 78 | | undefined, 79 | CancelComponent: undefined as 80 | | React.FC<{ onClick: (event: React.MouseEvent) => void; className: string; style: React.CSSProperties }> 81 | | React.Component<{ onClick: (event: React.MouseEvent) => void; className: string; style: React.CSSProperties }> 82 | | undefined, 83 | DoneComponent: undefined as 84 | | React.FC<{ onClick: (event: React.MouseEvent) => void; className: string; style: React.CSSProperties }> 85 | | React.Component<{ onClick: (event: React.MouseEvent) => void; className: string; style: React.CSSProperties }> 86 | | undefined, 87 | CustomOperation: undefined as React.FC<{ node: any }> | React.Component<{ node: any }> | undefined 88 | }) 89 | 90 | export interface JsonViewProps { 91 | src: any 92 | 93 | collapseStringsAfterLength?: number 94 | collapseStringMode?: 'directly' | 'word' | 'address' 95 | customizeCollapseStringUI?: CustomizeCollapseStringUI 96 | 97 | collapseObjectsAfterLength?: number 98 | collapsed?: Collapsed 99 | onCollapse?: OnCollapse 100 | 101 | enableClipboard?: boolean 102 | 103 | editable?: Editable 104 | onEdit?: OnEdit 105 | onDelete?: OnDelete 106 | onAdd?: OnAdd 107 | onChange?: OnChange 108 | 109 | customizeNode?: CustomizeNode 110 | customizeCopy?: (node: any, nodeMeta?: NodeMeta) => any 111 | 112 | dark?: boolean 113 | theme?: 'default' | 'a11y' | 'github' | 'vscode' | 'atom' | 'winter-is-coming' 114 | 115 | displaySize?: DisplaySize 116 | displayArrayIndex?: boolean 117 | 118 | style?: React.CSSProperties 119 | className?: string 120 | 121 | matchesURL?: boolean 122 | urlRegExp?: RegExp 123 | 124 | ignoreLargeArray?: boolean 125 | 126 | CopyComponent?: 127 | | React.FC<{ onClick: (event: React.MouseEvent) => void; className: string }> 128 | | React.Component<{ onClick: (event: React.MouseEvent) => void; className: string }> 129 | CopiedComponent?: React.FC<{ className: string; style: React.CSSProperties }> | React.Component<{ className: string; style: React.CSSProperties }> 130 | 131 | EditComponent?: 132 | | React.FC<{ onClick: (event: React.MouseEvent) => void; className: string }> 133 | | React.Component<{ onClick: (event: React.MouseEvent) => void; className: string }> 134 | 135 | CancelComponent?: 136 | | React.FC<{ onClick: (event: React.MouseEvent) => void; className: string; style: React.CSSProperties }> 137 | | React.Component<{ onClick: (event: React.MouseEvent) => void; className: string; style: React.CSSProperties }> 138 | 139 | DoneComponent?: 140 | | React.FC<{ onClick: (event: React.MouseEvent) => void; className: string; style: React.CSSProperties }> 141 | | React.Component<{ onClick: (event: React.MouseEvent) => void; className: string; style: React.CSSProperties }> 142 | 143 | CustomOperation?: React.FC<{ node: any }> | React.Component<{ node: any }> 144 | } 145 | 146 | export default function JsonView({ 147 | src: _src, 148 | 149 | collapseStringsAfterLength = 99, 150 | collapseStringMode = 'directly', 151 | customizeCollapseStringUI, 152 | 153 | collapseObjectsAfterLength = 99, 154 | collapsed, 155 | onCollapse, 156 | 157 | enableClipboard = true, 158 | 159 | editable = false, 160 | onEdit, 161 | onDelete, 162 | onAdd, 163 | onChange, 164 | 165 | dark = false, 166 | theme = 'default', 167 | 168 | customizeNode, 169 | customizeCopy = node => stringifyForCopying(node), 170 | 171 | displaySize, 172 | displayArrayIndex = true, 173 | 174 | style, 175 | className, 176 | 177 | matchesURL = false, 178 | urlRegExp = defaultURLRegExp, 179 | 180 | ignoreLargeArray = false, 181 | 182 | CopyComponent, 183 | CopiedComponent, 184 | 185 | EditComponent, 186 | CancelComponent, 187 | DoneComponent, 188 | CustomOperation 189 | }: JsonViewProps) { 190 | const [_, update] = useState(0) 191 | const forceUpdate = useCallback(() => update(state => ++state), []) 192 | const [src, setSrc] = useState(_src) 193 | useEffect(() => setSrc(_src), [_src]) 194 | return ( 195 | 235 | 238 | { 242 | setSrc(newValue) 243 | if (onEdit) 244 | onEdit({ 245 | newValue, 246 | oldValue, 247 | depth: 1, 248 | src, 249 | indexOrName: indexOrName, 250 | parentType: null, 251 | parentPath: parentPath 252 | }) 253 | if (onChange) onChange({ type: 'edit', depth: 1, src, indexOrName: indexOrName, parentType: null, parentPath: parentPath }) 254 | }} 255 | deleteHandle={(indexOrName: number | string, parentPath: string[]) => { 256 | setSrc(undefined) 257 | if (onDelete) 258 | onDelete({ 259 | value: src, 260 | depth: 1, 261 | src, 262 | indexOrName: indexOrName, 263 | parentType: null, 264 | parentPath: parentPath 265 | }) 266 | if (onChange) 267 | onChange({ 268 | depth: 1, 269 | src, 270 | indexOrName: indexOrName, 271 | parentType: null, 272 | type: 'delete', 273 | parentPath: parentPath 274 | }) 275 | }} 276 | parentPath={[]} 277 | /> 278 | 279 | 280 | ) 281 | } 282 | -------------------------------------------------------------------------------- /src/components/large-array-node.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useState } from 'react' 2 | import { JsonViewContext } from './json-view' 3 | import { customCopy, objectSize, ifDisplay } from '../utils' 4 | import { ReactComponent as AngleDownSVG } from '../svgs/angle-down.svg' 5 | import CopyButton from './copy-button' 6 | import NameValue from './name-value' 7 | import type { CustomizeOptions } from '../types' 8 | 9 | interface Props { 10 | originNode: Array 11 | node: Array 12 | depth: number 13 | index: number 14 | deleteHandle?: (_: string | number, currentPath: string[]) => void 15 | customOptions?: CustomizeOptions 16 | startIndex: number 17 | parent?: Record | Array 18 | parentPath: string[] 19 | } 20 | 21 | export default function LargeArrayNode({ originNode, node, depth, index, deleteHandle: _deleteSelf, customOptions, startIndex, parent, parentPath }: Props) { 22 | const { enableClipboard, src, onEdit, onChange, forceUpdate, displaySize, CustomOperation } = useContext(JsonViewContext) 23 | 24 | const currentPath = [...parentPath, String(index)] 25 | 26 | const [fold, setFold] = useState(true) 27 | 28 | // Edit property 29 | const editHandle = useCallback( 30 | (indexOrName: number | string, newValue: any, oldValue: any) => { 31 | originNode[indexOrName as number] = newValue 32 | if (onEdit) 33 | onEdit({ 34 | newValue, 35 | oldValue, 36 | depth, 37 | src, 38 | indexOrName, 39 | parentType: 'array', 40 | parentPath 41 | }) 42 | if (onChange) onChange({ type: 'edit', depth, src, indexOrName, parentType: 'array', parentPath }) 43 | forceUpdate() 44 | }, 45 | [node, onEdit, onChange, forceUpdate] 46 | ) 47 | 48 | // Delete property 49 | const deleteHandle = (index: number | string) => { 50 | originNode.splice(index as number, 1) 51 | if (_deleteSelf) _deleteSelf(index, parentPath) 52 | forceUpdate() 53 | } 54 | 55 | const Icons = ( 56 | <> 57 | {!fold && ( 58 | setFold(true)} className='jv-size-chevron'> 59 | {ifDisplay(displaySize, depth, fold) && {objectSize(node)} Items} 60 | 61 | 62 | 63 | )} 64 | 65 | {!fold && enableClipboard && customCopy(customOptions) && ( 66 | 67 | )} 68 | {typeof CustomOperation === 'function' ? : null} 69 | 70 | ) 71 | 72 | return ( 73 |
74 | {'['} 75 | 76 | {Icons} 77 | 78 | {!fold ? ( 79 |
80 | {node.map((n, i) => ( 81 | 91 | ))} 92 |
93 | ) : ( 94 | 97 | )} 98 | 99 | {']'} 100 |
101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /src/components/large-array.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react' 2 | import { JsonViewContext } from './json-view' 3 | import { isCollapsed_largeArray, ifDisplay, editableAdd, editableDelete, customAdd, customCopy, customDelete } from '../utils' 4 | import { ReactComponent as AngleDownSVG } from '../svgs/angle-down.svg' 5 | import CopyButton from './copy-button' 6 | import { ReactComponent as DeleteSVG } from '../svgs/trash.svg' 7 | import { ReactComponent as AddSVG } from '../svgs/add-square.svg' 8 | import { ReactComponent as DoneSVG } from '../svgs/done.svg' 9 | import { ReactComponent as CancelSVG } from '../svgs/cancel.svg' 10 | import type { CustomizeOptions } from '../types' 11 | import LargeArrayNode from './large-array-node' 12 | 13 | interface Props { 14 | node: Array 15 | depth: number 16 | indexOrName?: number | string 17 | deleteHandle?: (_: string | number, currentPath: string[]) => void 18 | customOptions?: CustomizeOptions 19 | parent?: Record | Array 20 | parentPath: string[] 21 | } 22 | 23 | export default function LargeArray({ node, depth, deleteHandle: _deleteSelf, indexOrName, customOptions, parent, parentPath }: Props) { 24 | const currentPath = typeof indexOrName !== 'undefined' ? [...parentPath, String(indexOrName)] : parentPath 25 | 26 | const nestCollapsedArray: any[] = [] 27 | for (let i = 0; i < node.length; i += 100) { 28 | nestCollapsedArray.push(node.slice(i, i + 100)) 29 | } 30 | 31 | const { collapsed, enableClipboard, collapseObjectsAfterLength, editable, onDelete, src, onAdd, CustomOperation, onChange, forceUpdate, displaySize } = 32 | useContext(JsonViewContext) 33 | 34 | const [fold, setFold] = useState(isCollapsed_largeArray(node, depth, indexOrName, collapsed, collapseObjectsAfterLength, customOptions)) 35 | useEffect(() => { 36 | setFold(isCollapsed_largeArray(node, depth, indexOrName, collapsed, collapseObjectsAfterLength, customOptions)) 37 | }, [collapsed, collapseObjectsAfterLength]) 38 | 39 | // Delete self 40 | const [deleting, setDeleting] = useState(false) 41 | const deleteSelf = () => { 42 | setDeleting(false) 43 | if (_deleteSelf) _deleteSelf(indexOrName!, parentPath) 44 | if (onDelete) onDelete({ value: node, depth, src, indexOrName: indexOrName!, parentType: 'array', parentPath }) 45 | if (onChange) 46 | onChange({ 47 | type: 'delete', 48 | depth, 49 | src, 50 | indexOrName: indexOrName!, 51 | parentType: 'array', 52 | parentPath 53 | }) 54 | } 55 | 56 | // Add 57 | const [adding, setAdding] = useState(false) 58 | const add = () => { 59 | const arr = node as unknown as any[] 60 | arr.push(null) 61 | if (onAdd) onAdd({ indexOrName: arr.length - 1, depth, src, parentType: 'array', parentPath }) 62 | if (onChange) onChange({ type: 'add', indexOrName: arr.length - 1, depth, src, parentType: 'array', parentPath }) 63 | forceUpdate() 64 | } 65 | 66 | const isEditing = deleting || adding 67 | const cancel = () => { 68 | setDeleting(false) 69 | setAdding(false) 70 | } 71 | 72 | const Icons = ( 73 | <> 74 | {!fold && !isEditing && ( 75 | setFold(true)} className='jv-size-chevron'> 76 | {ifDisplay(displaySize, depth, fold) && {node.length} Items} 77 | 78 | 79 | 80 | )} 81 | 82 | {isEditing && } 83 | {isEditing && } 84 | 85 | {!fold && !isEditing && enableClipboard && customCopy(customOptions) && ( 86 | 87 | )} 88 | {!fold && !isEditing && editableAdd(editable) && customAdd(customOptions) && ( 89 | { 92 | add() 93 | }} 94 | /> 95 | )} 96 | {!fold && !isEditing && editableDelete(editable) && customDelete(customOptions) && _deleteSelf && ( 97 | setDeleting(true)} /> 98 | )} 99 | {typeof CustomOperation === 'function' ? : null} 100 | 101 | ) 102 | 103 | return ( 104 | <> 105 | {'['} 106 | 107 | {Icons} 108 | 109 | {!fold ? ( 110 |
111 | {nestCollapsedArray.map((item, index) => ( 112 | 123 | ))} 124 |
125 | ) : ( 126 | 129 | )} 130 | 131 | {']'} 132 | 133 | {fold && ifDisplay(displaySize, depth, fold) && ( 134 | setFold(false)} className='jv-size'> 135 | {node.length} Items 136 | 137 | )} 138 | 139 | ) 140 | } 141 | -------------------------------------------------------------------------------- /src/components/long-string.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef, useState } from 'react' 2 | import { JsonViewContext } from './json-view' 3 | 4 | interface Props { 5 | str: string 6 | className: string 7 | ctrlClick: ((event: React.MouseEvent) => void) | undefined 8 | } 9 | 10 | const LongString = React.forwardRef(({ str, className, ctrlClick }, ref) => { 11 | let { collapseStringMode, collapseStringsAfterLength, customizeCollapseStringUI } = useContext(JsonViewContext) 12 | const [truncated, setTruncated] = useState(true) 13 | const strRef = useRef(null) 14 | 15 | collapseStringsAfterLength = collapseStringsAfterLength > 0 ? collapseStringsAfterLength : 0 16 | 17 | const str_show = str.replace(/\s+/g, ' ') 18 | const collapseStringUI = 19 | typeof customizeCollapseStringUI === 'function' 20 | ? customizeCollapseStringUI(str_show, truncated) 21 | : typeof customizeCollapseStringUI === 'string' 22 | ? customizeCollapseStringUI 23 | : '...' 24 | 25 | const clickToTruncateOrEdit = (event: React.MouseEvent) => { 26 | if ((event.ctrlKey || event.metaKey) && ctrlClick) { 27 | ctrlClick(event) 28 | } else { 29 | const selection = window.getSelection() 30 | 31 | if (selection && selection.anchorOffset !== selection.focusOffset && selection.anchorNode?.parentElement === strRef.current) return 32 | 33 | setTruncated(!truncated) 34 | } 35 | } 36 | 37 | if (str.length <= collapseStringsAfterLength) 38 | return ( 39 | 40 | "{str}" 41 | 42 | ) 43 | 44 | if (collapseStringMode === 'address') 45 | return str.length <= 10 ? ( 46 | 47 | "{str}" 48 | 49 | ) : ( 50 | 51 | "{truncated ? [str_show.slice(0, 6), collapseStringUI, str_show.slice(-4)] : str}" 52 | 53 | ) 54 | 55 | if (collapseStringMode === 'directly') { 56 | return ( 57 | 58 | "{truncated ? [str_show.slice(0, collapseStringsAfterLength), collapseStringUI] : str}" 59 | 60 | ) 61 | } 62 | 63 | if (collapseStringMode === 'word') { 64 | let index_ahead = collapseStringsAfterLength 65 | let index_behind = collapseStringsAfterLength + 1 66 | let str_collapsed = str_show 67 | let count = 1 68 | 69 | while (true) { 70 | if (/\W/.test(str[index_ahead])) { 71 | str_collapsed = str.slice(0, index_ahead) 72 | break 73 | } 74 | if (/\W/.test(str[index_behind])) { 75 | str_collapsed = str.slice(0, index_behind) 76 | break 77 | } 78 | if (count === 6) { 79 | str_collapsed = str.slice(0, collapseStringsAfterLength) 80 | break 81 | } 82 | 83 | count++ 84 | index_ahead-- 85 | index_behind++ 86 | } 87 | 88 | return ( 89 | 90 | "{truncated ? [str_collapsed, collapseStringUI] : str}" 91 | 92 | ) 93 | } 94 | 95 | return ( 96 | 97 | "{str}" 98 | 99 | ) 100 | }) 101 | 102 | export default LongString 103 | -------------------------------------------------------------------------------- /src/components/name-value.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { JsonViewContext } from './json-view' 3 | 4 | import JsonNode from './json-node' 5 | 6 | interface Props { 7 | indexOrName: number | string 8 | value: any 9 | depth: number 10 | parent?: Record | Array 11 | parentPath: string[] 12 | deleteHandle: (indexOrName: string | number, parentPath: string[]) => void 13 | editHandle: (indexOrName: string | number, newValue: any, oldValue: any, parentPath: string[]) => void 14 | } 15 | 16 | export default function NameValue({ indexOrName, value, depth, deleteHandle, editHandle, parent, parentPath }: Props) { 17 | const { displayArrayIndex } = useContext(JsonViewContext) 18 | const isArray = Array.isArray(parent) 19 | 20 | return ( 21 |
22 | {!isArray || (isArray && displayArrayIndex) ? ( 23 | <> 24 | {indexOrName}:{' '} 25 | 26 | ) : ( 27 | <> 28 | )} 29 | deleteHandle(indexOrName, parentPath)} 33 | editHandle={(indexOrName, newValue, oldValue, parentPath) => editHandle(indexOrName, newValue, oldValue, parentPath)} 34 | parent={parent} 35 | indexOrName={indexOrName} 36 | parentPath={parentPath} 37 | /> 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/object-node.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useEffect, useRef, useState } from 'react' 2 | import { JsonViewContext } from './json-view' 3 | import { isObject, customAdd, customCopy, customDelete, editableAdd, editableDelete, isCollapsed, objectSize, ifDisplay } from '../utils' 4 | import { ReactComponent as AngleDownSVG } from '../svgs/angle-down.svg' 5 | import CopyButton from './copy-button' 6 | import NameValue from './name-value' 7 | import { ReactComponent as DeleteSVG } from '../svgs/trash.svg' 8 | import { ReactComponent as AddSVG } from '../svgs/add-square.svg' 9 | import { ReactComponent as DoneSVG } from '../svgs/done.svg' 10 | import { ReactComponent as CancelSVG } from '../svgs/cancel.svg' 11 | import type { CustomizeOptions } from '../types' 12 | import LargeArray from './large-array' 13 | 14 | interface Props { 15 | node: Record | Array 16 | depth: number 17 | indexOrName?: number | string 18 | deleteHandle?: (_: string | number, currentPath: string[]) => void 19 | customOptions?: CustomizeOptions 20 | parent?: Record | Array 21 | parentPath: string[] 22 | } 23 | 24 | export default function ObjectNode({ node, depth, indexOrName, deleteHandle: _deleteSelf, customOptions, parent, parentPath }: Props) { 25 | const { 26 | collapsed, 27 | onCollapse, 28 | enableClipboard, 29 | ignoreLargeArray, 30 | collapseObjectsAfterLength, 31 | editable, 32 | onDelete, 33 | src, 34 | onAdd, 35 | onEdit, 36 | onChange, 37 | forceUpdate, 38 | displaySize, 39 | CustomOperation 40 | } = useContext(JsonViewContext) 41 | 42 | const currentPath = typeof indexOrName !== 'undefined' ? [...parentPath, String(indexOrName)] : parentPath 43 | 44 | if (!ignoreLargeArray && Array.isArray(node) && node.length > 100) { 45 | return 46 | } 47 | 48 | const isPlainObject = isObject(node) 49 | 50 | const [fold, _setFold] = useState(isCollapsed(node, depth, indexOrName, collapsed, collapseObjectsAfterLength, customOptions)) 51 | 52 | const setFold = (value: boolean) => { 53 | onCollapse?.({ isCollapsing: !value, node, depth, indexOrName }) 54 | _setFold(value) 55 | } 56 | 57 | useEffect(() => { 58 | setFold(isCollapsed(node, depth, indexOrName, collapsed, collapseObjectsAfterLength, customOptions)) 59 | }, [collapsed, collapseObjectsAfterLength]) 60 | 61 | // Edit property 62 | const editHandle = useCallback( 63 | (indexOrName: number | string, newValue: any, oldValue: any) => { 64 | if (Array.isArray(node)) { 65 | node[+indexOrName] = newValue 66 | } else if (node) { 67 | node[indexOrName] = newValue 68 | } 69 | if (onEdit) 70 | onEdit({ 71 | newValue, 72 | oldValue, 73 | depth, 74 | src, 75 | indexOrName: indexOrName, 76 | parentType: isPlainObject ? 'object' : 'array', 77 | parentPath: currentPath 78 | }) 79 | if (onChange) onChange({ type: 'edit', depth, src, indexOrName: indexOrName, parentType: isPlainObject ? 'object' : 'array', parentPath: currentPath }) 80 | forceUpdate() 81 | }, 82 | [node, onEdit, onChange, forceUpdate] 83 | ) 84 | 85 | // Delete property 86 | const deleteHandle = (indexOrName: number | string) => { 87 | if (Array.isArray(node)) { 88 | node.splice(+indexOrName, 1) 89 | } else if (node) { 90 | delete node[indexOrName] 91 | } 92 | forceUpdate() 93 | } 94 | 95 | // Delete self 96 | const [deleting, setDeleting] = useState(false) 97 | const deleteSelf = () => { 98 | setDeleting(false) 99 | if (_deleteSelf) _deleteSelf(indexOrName!, currentPath) 100 | if (onDelete) onDelete({ value: node, depth, src, indexOrName: indexOrName!, parentType: isPlainObject ? 'object' : 'array', parentPath: currentPath }) 101 | if (onChange) 102 | onChange({ 103 | type: 'delete', 104 | depth, 105 | src, 106 | indexOrName: indexOrName!, 107 | parentType: isPlainObject ? 'object' : 'array', 108 | parentPath: currentPath 109 | }) 110 | } 111 | 112 | // Add 113 | const [adding, setAdding] = useState(false) 114 | const inputRef = useRef(null) 115 | const add = () => { 116 | if (isPlainObject) { 117 | const inputName = inputRef.current?.value 118 | 119 | if (inputName) { 120 | ;(node as Record)[inputName] = null 121 | 122 | if (inputRef.current) inputRef.current.value = '' 123 | setAdding(false) 124 | 125 | if (onAdd) onAdd({ indexOrName: inputName, depth, src, parentType: 'object', parentPath: currentPath }) 126 | if (onChange) onChange({ type: 'add', indexOrName: inputName, depth, src, parentType: 'object', parentPath: currentPath }) 127 | } 128 | } else if (Array.isArray(node)) { 129 | const arr = node as unknown as any[] 130 | arr.push(null) 131 | if (onAdd) onAdd({ indexOrName: arr.length - 1, depth, src, parentType: 'array', parentPath: currentPath }) 132 | if (onChange) onChange({ type: 'add', indexOrName: arr.length - 1, depth, src, parentType: 'array', parentPath: currentPath }) 133 | } 134 | forceUpdate() 135 | } 136 | const handleAddKeyDown = (event: React.KeyboardEvent) => { 137 | if (event.key === 'Enter') { 138 | event.preventDefault() 139 | add() 140 | } else if (event.key === 'Escape') { 141 | cancel() 142 | } 143 | } 144 | 145 | const isEditing = deleting || adding 146 | const cancel = () => { 147 | setDeleting(false) 148 | setAdding(false) 149 | } 150 | 151 | const Icons = ( 152 | <> 153 | {!fold && !isEditing && ( 154 | setFold(true)} className='jv-size-chevron'> 155 | {ifDisplay(displaySize, depth, fold) && {objectSize(node)} Items} 156 | 157 | 158 | 159 | )} 160 | 161 | {adding && isPlainObject && } 162 | 163 | {isEditing && } 164 | {isEditing && } 165 | 166 | {!fold && !isEditing && enableClipboard && customCopy(customOptions) && ( 167 | 168 | )} 169 | {!fold && !isEditing && editableAdd(editable) && customAdd(customOptions) && ( 170 | { 173 | if (isPlainObject) { 174 | setAdding(true) 175 | setTimeout(() => inputRef.current?.focus()) 176 | } else { 177 | add() 178 | } 179 | }} 180 | /> 181 | )} 182 | {!fold && !isEditing && editableDelete(editable) && customDelete(customOptions) && _deleteSelf && ( 183 | setDeleting(true)} /> 184 | )} 185 | {typeof CustomOperation === 'function' ? : null} 186 | 187 | ) 188 | 189 | if (Array.isArray(node)) { 190 | return ( 191 | <> 192 | {'['} 193 | 194 | {Icons} 195 | 196 | {!fold ? ( 197 |
198 | {node.map((n, i) => ( 199 | 209 | ))} 210 |
211 | ) : ( 212 | 215 | )} 216 | 217 | {']'} 218 | 219 | {fold && ifDisplay(displaySize, depth, fold) && ( 220 | setFold(false)} className='jv-size'> 221 | {objectSize(node)} Items 222 | 223 | )} 224 | 225 | ) 226 | } else if (isPlainObject) { 227 | return ( 228 | <> 229 | {'{'} 230 | 231 | {Icons} 232 | 233 | {!fold ? ( 234 |
235 | {Object.entries(node).map(([name, value]) => ( 236 | 246 | ))} 247 |
248 | ) : ( 249 | 252 | )} 253 | 254 | {'}'} 255 | 256 | {fold && ifDisplay(displaySize, depth, fold) && ( 257 | setFold(false)} className='jv-size'> 258 | {objectSize(node)} Items 259 | 260 | )} 261 | 262 | ) 263 | } else { 264 | return {String(node)} 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/dark.css: -------------------------------------------------------------------------------- 1 | :is(.dark .json-view, .dark.json-view) { 2 | color: #d1d1d1; 3 | --json-property: #009033; 4 | --json-index: #5d75f2; 5 | --json-number: #5d75f2; 6 | --json-string: #c57e29; 7 | --json-boolean: #e4407b; 8 | --json-null: #e4407b; 9 | } 10 | :is(.dark .json-view_a11y, .dark.json-view_a11y) { 11 | color: #d1d1d1; 12 | --json-property: #ffd700; 13 | --json-index: #00e0e0; 14 | --json-number: #00e0e0; 15 | --json-string: #abe338; 16 | --json-boolean: #ffa07a; 17 | --json-null: #ffa07a; 18 | } 19 | :is(.dark .json-view_github, .dark.json-view_github) { 20 | color: #79b8ff; 21 | --json-property: #79b8ff; 22 | --json-index: #79b8ff; 23 | --json-number: #79b8ff; 24 | --json-string: #9ecbff; 25 | --json-boolean: #79b8ff; 26 | --json-null: #79b8ff; 27 | } 28 | :is(.dark .json-view_vscode, .dark.json-view_vscode) { 29 | color: #da70d6; 30 | --json-property: #9cdcfe; 31 | --json-index: #b5cea8; 32 | --json-number: #b5cea8; 33 | --json-string: #ce9178; 34 | --json-boolean: #569cd6; 35 | --json-null: #569cd6; 36 | } 37 | :is(.dark .json-view_atom, .dark.json-view_atom) { 38 | color: #abb2bf; 39 | --json-property: #e06c75; 40 | --json-index: #d19a66; 41 | --json-number: #d19a66; 42 | --json-string: #98c379; 43 | --json-boolean: #56b6c2; 44 | --json-null: #56b6c2; 45 | } 46 | :is(.dark .json-view_winter-is-coming, .dark.json-view_winter-is-coming) { 47 | color: #a7dbf7; 48 | --json-property: #91dacd; 49 | --json-index: #8dec95; 50 | --json-number: #8dec95; 51 | --json-string: #e0aff5; 52 | --json-boolean: #f29fd8; 53 | --json-null: #f29fd8; 54 | } 55 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const src: string 3 | export default src 4 | export const ReactComponent: React.FC> 5 | } 6 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import JsonView, { defaultURLRegExp, JsonViewProps } from './components/json-view' 2 | import { stringifyForCopying as stringify } from './utils' 3 | 4 | import { ReactComponent as EditSVG } from './svgs/edit.svg' 5 | import { ReactComponent as DeleteSVG } from './svgs/trash.svg' 6 | import { ReactComponent as DoneSVG } from './svgs/done.svg' 7 | import { ReactComponent as CancelSVG } from './svgs/cancel.svg' 8 | import { ReactComponent as CopySVG } from './svgs/copy.svg' 9 | import { ReactComponent as CopiedSVG } from './svgs/copied.svg' 10 | import { ReactComponent as LinkSVG } from './svgs/link.svg' 11 | 12 | export { JsonView as default, stringify, defaultURLRegExp, EditSVG, DeleteSVG, DoneSVG, CancelSVG, CopySVG, CopiedSVG, LinkSVG } 13 | export type { JsonViewProps } 14 | -------------------------------------------------------------------------------- /src/stories/editable.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Meta, StoryObj } from '@storybook/react' 3 | import JsonView from '../index' 4 | import { argTypes, largeArray } from './share' 5 | import { stringifyForCopying } from '../utils' 6 | 7 | type TYPE_FC = typeof JsonView 8 | 9 | export default { 10 | title: 'Editable', 11 | component: JsonView, 12 | 13 | argTypes, 14 | args: { 15 | enableClipboard: true, 16 | editable: true, 17 | src: { 18 | string: 'string', 19 | longString: 'long string long string long string long string long string long string', 20 | number: 123456, 21 | boolean: false, 22 | null: null, 23 | func: function () { 24 | console.log('Hello World') 25 | }, 26 | Symbol: Symbol('JSON View'), 27 | obj: { 28 | k1: 123, 29 | k2: '123', 30 | k3: false, 31 | nested: { 32 | k4: 'nested' 33 | } 34 | }, 35 | arr: ['string', 123456, false, null] 36 | }, 37 | onEdit: ({ newValue, src, oldValue, indexOrName, parentPath }) => { 38 | console.log('[onEdit]', indexOrName, newValue, oldValue, src, parentPath) 39 | }, 40 | onDelete: ({ value, src, indexOrName, parentPath }) => { 41 | console.log('[onDelete]', indexOrName, value, src, parentPath) 42 | }, 43 | onAdd: ({ src, indexOrName, parentPath }) => { 44 | console.log('[onAdd]', indexOrName, src, parentPath) 45 | }, 46 | onChange: ({ src, indexOrName, parentPath }) => { 47 | console.log('[onChange]', indexOrName, src, parentPath) 48 | } 49 | }, 50 | decorators: [ 51 | Story => ( 52 |
55 |
56 | 57 |
58 |
59 | ) 60 | ] 61 | } as Meta 62 | 63 | export const Primary: StoryObj = {} 64 | 65 | export const DisplaySize: StoryObj = { 66 | args: { 67 | src: { 68 | string: 'string', 69 | longString: 'long string long string long string long string long string long string', 70 | number: 123456, 71 | boolean: false, 72 | null: null, 73 | func: function () {}, 74 | Symbol: Symbol('JSON View'), 75 | arr: ['string', 123456, false, null], 76 | nest: { 77 | nest: { 78 | nest: { 79 | nest: { 80 | over: 'over' 81 | } 82 | } 83 | } 84 | } 85 | }, 86 | displaySize: 'collapsed' 87 | } 88 | } 89 | 90 | export const XS: StoryObj = { 91 | decorators: [ 92 | Story => ( 93 |
94 | 95 |
96 | ) 97 | ] 98 | } 99 | export const LG: StoryObj = { 100 | decorators: [ 101 | Story => ( 102 |
103 | 104 |
105 | ) 106 | ] 107 | } 108 | export const XL: StoryObj = { 109 | decorators: [ 110 | Story => ( 111 |
112 | 113 |
114 | ) 115 | ] 116 | } 117 | 118 | export const String: StoryObj = { 119 | args: { 120 | src: 'string' 121 | } 122 | } 123 | 124 | export const LongString: StoryObj = { 125 | args: { 126 | src: 'long string long string long string long string' 127 | } 128 | } 129 | 130 | export const Number: StoryObj = { 131 | args: { 132 | src: 12312312321 133 | } 134 | } 135 | 136 | export const Null: StoryObj = { 137 | args: { 138 | src: null 139 | } 140 | } 141 | 142 | export const Boolean: StoryObj = { 143 | args: { 144 | src: true 145 | } 146 | } 147 | 148 | export const Undefined: StoryObj = { 149 | args: { 150 | src: undefined 151 | } 152 | } 153 | 154 | export const Collapsed_Boolean: StoryObj = { 155 | args: { 156 | src: { 157 | string: 'string', 158 | longString: 'long string long string long string long string long string long string', 159 | number: 123456, 160 | boolean: false, 161 | null: null, 162 | func: function () {}, 163 | Symbol: Symbol('JSON View'), 164 | obj: { 165 | k1: 123, 166 | k2: '123', 167 | k3: false 168 | }, 169 | arr: ['string', 123456, false, null] 170 | }, 171 | collapsed: true 172 | }, 173 | decorators: [ 174 | Story => ( 175 |
176 | 177 |
178 | ) 179 | ] 180 | } 181 | 182 | export const Collapsed_Number: StoryObj = { 183 | args: { 184 | src: { 185 | boolean: false, 186 | null: null, 187 | obj: { 188 | k1: 123, 189 | k2: '123', 190 | k3: false, 191 | k4: { 192 | k: { 193 | k: { 194 | k: 'k5' 195 | } 196 | } 197 | } 198 | }, 199 | arr: ['string', 123456, false, null, [123, 123, 123, [123, 123, 123]]] 200 | }, 201 | collapsed: 2 202 | }, 203 | decorators: [ 204 | Story => ( 205 |
206 | 207 |
208 | ) 209 | ] 210 | } 211 | 212 | export const Editable_Options: StoryObj = { 213 | args: { 214 | src: { 215 | boolean: false, 216 | null: null, 217 | obj: { 218 | k1: 123, 219 | k2: '123', 220 | k3: false, 221 | k4: { 222 | k: { 223 | k: { 224 | k: 'k5' 225 | } 226 | } 227 | } 228 | }, 229 | arr: ['string', 123456, false, null, [123, 123, 123, [123, 123, 123]]] 230 | }, 231 | editable: { 232 | add: false, 233 | edit: true, 234 | delete: false 235 | } 236 | }, 237 | decorators: [ 238 | Story => ( 239 |
240 | 241 |
242 | ) 243 | ] 244 | } 245 | 246 | export const MatchesURL: StoryObj = { 247 | args: { 248 | src: { 249 | string: 'string', 250 | link: 'https://www.google.com/', 251 | number: 123456, 252 | boolean: false, 253 | null: null, 254 | func: function () {}, 255 | Symbol: Symbol('JSON View'), 256 | obj: { 257 | k1: 123, 258 | k2: '123', 259 | k3: false 260 | }, 261 | arr: ['string', 123456, false, null] 262 | }, 263 | matchesURL: true, 264 | editable: true 265 | } 266 | } 267 | 268 | export const CustomizeCopy: StoryObj = { 269 | args: { 270 | src: { 271 | string: 'string', 272 | link: 'https://www.google.com/', 273 | number: 123456, 274 | boolean: false, 275 | null: null, 276 | func: function () {}, 277 | Symbol: Symbol('JSON View'), 278 | obj: { 279 | k1: 123, 280 | k2: '123', 281 | k3: false 282 | }, 283 | arr: ['string', 123456, false, null] 284 | }, 285 | editable: true, 286 | customizeCopy: (node: any) => stringifyForCopying(node, 4) 287 | } 288 | } 289 | 290 | export const LargeArray: StoryObj = { 291 | args: { 292 | src: { 293 | obj: { 294 | k1: 123, 295 | k2: '123', 296 | k3: false 297 | }, 298 | largeArray: largeArray 299 | }, 300 | editable: true, 301 | displaySize: true, 302 | collapsed: 2 303 | } 304 | } 305 | 306 | export const CustomIcons: StoryObj = { 307 | args: { 308 | src: { 309 | obj: { 310 | k1: 123, 311 | k2: '123', 312 | k3: false 313 | } 314 | }, 315 | editable: true, 316 | displaySize: true, 317 | CopyComponent: ({ onClick, className }) => ( 318 | 319 | 320 | 321 | ), 322 | CopiedComponent: ({ className, style }) => ( 323 | 324 | 325 | 326 | ), 327 | EditComponent: ({ onClick, className }) => ( 328 | 329 | ), 330 | CancelComponent: ({ onClick, className }) => ( 331 | 332 | ), 333 | DoneComponent: ({ onClick, className }) => ( 334 | 335 | ), 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/stories/json-view.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Meta, StoryObj } from '@storybook/react' 3 | import JsonView from '../index' 4 | import { argTypes, largeArray } from './share' 5 | import { stringifyForCopying } from '../utils' 6 | 7 | type TYPE_FC = typeof JsonView 8 | 9 | export default { 10 | title: 'JSON View', 11 | component: JsonView, 12 | 13 | argTypes, 14 | decorators: [ 15 | Story => ( 16 |
19 |
20 | 21 |
22 |
23 | ) 24 | ] 25 | } as Meta 26 | 27 | export const Primary: StoryObj = { 28 | args: { 29 | src: { 30 | string: 'string', 31 | longString: 'long string long string long string long string long string long string', 32 | number: 123456, 33 | boolean: false, 34 | null: null, 35 | func: function () {}, 36 | Symbol: Symbol('JSON View'), 37 | obj: { 38 | k1: 123, 39 | k2: '123', 40 | k3: false 41 | }, 42 | arr: ['string', 123456, false, null] 43 | } 44 | } 45 | } 46 | 47 | export const DisplaySize: StoryObj = { 48 | args: { 49 | src: { 50 | string: 'string', 51 | longString: 'long string long string long string long string long string long string', 52 | number: 123456, 53 | boolean: false, 54 | null: null, 55 | func: function () {}, 56 | Symbol: Symbol('JSON View'), 57 | arr: ['string', 123456, false, null] 58 | }, 59 | displaySize: true 60 | } 61 | } 62 | 63 | export const SM: StoryObj = { 64 | args: { 65 | src: { 66 | string: 'string', 67 | longString: 'long string long string long string long string long string long string', 68 | number: 123456, 69 | boolean: false, 70 | null: null, 71 | func: function () {}, 72 | Symbol: Symbol('JSON View'), 73 | arr: ['string', 123456, false, null] 74 | }, 75 | enableClipboard: true, 76 | displaySize: true 77 | }, 78 | decorators: [ 79 | Story => ( 80 |
81 | 82 |
83 | ) 84 | ] 85 | } 86 | 87 | export const XS: StoryObj = { 88 | args: { 89 | src: { 90 | string: 'string', 91 | longString: 'long string long string long string long string long string long string', 92 | number: 123456, 93 | boolean: false, 94 | null: null, 95 | func: function () {}, 96 | Symbol: Symbol('JSON View'), 97 | arr: ['string', 123456, false, null] 98 | }, 99 | enableClipboard: true, 100 | displaySize: true 101 | }, 102 | decorators: [ 103 | Story => ( 104 |
105 | 106 |
107 | ) 108 | ] 109 | } 110 | export const LG: StoryObj = { 111 | args: { 112 | src: { 113 | string: 'string', 114 | longString: 'long string long string long string long string long string long string', 115 | number: 123456, 116 | boolean: false, 117 | null: null, 118 | func: function () {}, 119 | Symbol: Symbol('JSON View'), 120 | arr: ['string', 123456, false, null] 121 | }, 122 | enableClipboard: true, 123 | displaySize: true 124 | }, 125 | decorators: [ 126 | Story => ( 127 |
128 | 129 |
130 | ) 131 | ] 132 | } 133 | export const XL: StoryObj = { 134 | args: { 135 | src: { 136 | string: 'string', 137 | longString: 'long string long string long string long string long string long string', 138 | number: 123456, 139 | boolean: false, 140 | null: null, 141 | func: function () {}, 142 | Symbol: Symbol('JSON View'), 143 | arr: ['string', 123456, false, null] 144 | }, 145 | enableClipboard: true, 146 | displaySize: true 147 | }, 148 | decorators: [ 149 | Story => ( 150 |
151 | 152 |
153 | ) 154 | ] 155 | } 156 | 157 | export const BigObject: StoryObj = { 158 | args: { 159 | src: { 160 | string: 'string', 161 | longString: 'long string long string long string long string', 162 | number: 123456, 163 | boolean: false, 164 | null: null, 165 | Date: new Date(), 166 | Symbol: Symbol('JSON View'), 167 | arr: ['string', 123456, false, null], 168 | 169 | bigObject: { 170 | abc0: 'abc', 171 | abc1: 'abc', 172 | abc2: 'abc', 173 | abc3: 'abc', 174 | abc4: 'abc', 175 | abc5: 'abc', 176 | abc6: 'abc', 177 | abc7: 'abc', 178 | abc8: 'abc', 179 | abc9: 'abc', 180 | abc10: 'abc', 181 | abc11: 'abc', 182 | abc12: 'abc', 183 | abc13: 'abc', 184 | abc14: 'abc', 185 | abc15: 'abc', 186 | abc16: 'abc', 187 | abc17: 'abc', 188 | abc18: 'abc', 189 | abc19: 'abc', 190 | abc20: 'abc', 191 | abc21: 'abc', 192 | abc22: 'abc', 193 | abc23: 'abc', 194 | abc24: 'abc', 195 | abc25: 'abc', 196 | abc26: 'abc', 197 | abc27: 'abc', 198 | abc28: 'abc', 199 | abc29: 'abc' 200 | } 201 | }, 202 | collapseObjectsAfterLength: 20 203 | } 204 | } 205 | 206 | export const Array: StoryObj = { 207 | args: { 208 | src: ['string', 123456, false, null, { string: 'string', number: 123456, boolean: false, null: null, Date: new Date(), Symbol: Symbol('JSON View') }], 209 | collapsed: 1 210 | } 211 | } 212 | 213 | export const BigArray: StoryObj = { 214 | args: { 215 | src: [ 216 | 'string', 217 | 123456, 218 | false, 219 | null, 220 | { string: 'string', number: 123456, boolean: false, null: null, Date: new Date(), Symbol: Symbol('JSON View') }, 221 | [ 222 | 'string', 223 | 123456, 224 | false, 225 | 'string', 226 | 123456, 227 | false, 228 | 'string', 229 | 123456, 230 | false, 231 | 'string', 232 | 123456, 233 | false, 234 | 'string', 235 | 123456, 236 | false, 237 | 'string', 238 | 123456, 239 | false, 240 | 'string', 241 | 123456, 242 | false, 243 | 'string', 244 | 123456, 245 | false 246 | ] 247 | ], 248 | collapseObjectsAfterLength: 20 249 | } 250 | } 251 | 252 | export const String: StoryObj = { 253 | args: { 254 | src: 'string' 255 | } 256 | } 257 | 258 | export const LongString: StoryObj = { 259 | args: { 260 | src: 'long string long string long string long string', 261 | collapseStringsAfterLength: 20, 262 | collapseStringMode: 'word' 263 | } 264 | } 265 | 266 | export const CustomizeCollapseStringUI: StoryObj = { 267 | args: { 268 | src: 'long string long string long string long string', 269 | collapseStringsAfterLength: 20, 270 | collapseStringMode: 'word', 271 | customizeCollapseStringUI: () => ( 272 | <> 273 | ... 274 | 275 | ) 276 | } 277 | } 278 | 279 | export const Number: StoryObj = { 280 | args: { 281 | src: 12312312321 282 | } 283 | } 284 | 285 | export const Null: StoryObj = { 286 | args: { 287 | src: null 288 | } 289 | } 290 | 291 | export const Boolean: StoryObj = { 292 | args: { 293 | src: true 294 | } 295 | } 296 | 297 | export const Undefined: StoryObj = { 298 | args: { 299 | src: undefined 300 | } 301 | } 302 | 303 | export const Collapsed_Boolean: StoryObj = { 304 | args: { 305 | src: { 306 | string: 'string', 307 | longString: 'long string long string long string long string long string long string', 308 | number: 123456, 309 | boolean: false, 310 | null: null, 311 | func: function () {}, 312 | Symbol: Symbol('JSON View'), 313 | obj: { 314 | k1: 123, 315 | k2: '123', 316 | k3: false 317 | }, 318 | arr: ['string', 123456, false, null] 319 | }, 320 | collapsed: true 321 | }, 322 | decorators: [ 323 | Story => ( 324 |
325 | 326 |
327 | ) 328 | ] 329 | } 330 | 331 | export const Collapsed_Number: StoryObj = { 332 | args: { 333 | src: { 334 | boolean: false, 335 | null: null, 336 | obj: { 337 | k1: 123, 338 | k2: '123', 339 | k3: false, 340 | k4: { 341 | k: { 342 | k: { 343 | k: 'k5' 344 | } 345 | } 346 | } 347 | }, 348 | arr: ['string', 123456, false, null, [123, 123, 123, [123, 123, 123]]] 349 | }, 350 | collapsed: 2 351 | }, 352 | decorators: [ 353 | Story => ( 354 |
355 | 356 |
357 | ) 358 | ] 359 | } 360 | 361 | export const Collapsed_Function: StoryObj = { 362 | args: { 363 | src: { 364 | boolean: false, 365 | null: null, 366 | obj: { 367 | k1: 123, 368 | k2: '123', 369 | k3: false, 370 | k4: { 371 | k: { 372 | k: { 373 | k: 'k5' 374 | } 375 | } 376 | } 377 | }, 378 | arr: ['string', 123456, false, null, [123, 123, 123, [123, 123, 123]]], 379 | arr2: [1, 2] 380 | }, 381 | collapsed: params => { 382 | if (params.depth > 3) return true 383 | if (params.depth > 2 && params.size > 4) return true 384 | return false 385 | } 386 | }, 387 | decorators: [ 388 | Story => ( 389 |
390 | 391 |
392 | ) 393 | ] 394 | } 395 | 396 | export const CustomizeNode: StoryObj = { 397 | args: { 398 | editable: true, 399 | src: { 400 | suni: 'suni', 401 | string: 'string', 402 | longString: 'long string long string long string long string long string long string', 403 | number: 123456, 404 | boolean: false, 405 | null: null, 406 | func: function () {}, 407 | Symbol: Symbol('JSON View'), 408 | obj: { 409 | k1: 123, 410 | k2: '123', 411 | k3: false 412 | }, 413 | arr: ['string', 123456, false, null, [1, 2, 3]] 414 | }, 415 | customizeNode: params => { 416 | if (params.node === 'suni') return () => suni 417 | if (params.node === 123) return 123 418 | if (params.indexOrName === 'obj') return { add: false, delete: false, enableClipboard: false } 419 | if (params.node === 'string') return { edit: true, enableClipboard: false, delete: false } 420 | if (params.indexOrName === 'arr') return { collapsed: false } 421 | if (params.depth > 2) return { collapsed: true } 422 | if (params.indexOrName === 'func') return { className: 'underline' } 423 | } 424 | }, 425 | decorators: [ 426 | Story => ( 427 |
428 | 429 |
430 | ) 431 | ] 432 | } 433 | 434 | export const MatchesURL: StoryObj = { 435 | args: { 436 | src: { 437 | string: 'string', 438 | link: 'https://www.google.com/', 439 | number: 123456, 440 | boolean: false, 441 | null: null, 442 | func: function () {}, 443 | Symbol: Symbol('JSON View'), 444 | obj: { 445 | k1: 123, 446 | k2: '123', 447 | k3: false 448 | }, 449 | arr: ['string', 123456, false, null] 450 | }, 451 | matchesURL: true 452 | } 453 | } 454 | 455 | export const CustomizeCopy: StoryObj = { 456 | args: { 457 | src: { 458 | string: 'string', 459 | link: 'https://www.google.com/', 460 | number: 123456, 461 | boolean: false, 462 | null: null, 463 | func: function () {}, 464 | Symbol: Symbol('JSON View'), 465 | obj: { 466 | k1: 123, 467 | k2: '123', 468 | k3: false 469 | }, 470 | arr: ['string', 123456, false, null] 471 | }, 472 | customizeCopy: (node: any) => stringifyForCopying(node, 4) 473 | } 474 | } 475 | 476 | export const LargeArray: StoryObj = { 477 | args: { 478 | src: { 479 | obj: { 480 | k1: 123, 481 | k2: '123', 482 | k3: false 483 | }, 484 | largeArray: largeArray 485 | }, 486 | displaySize: true 487 | } 488 | } 489 | 490 | export const CustomIcons: StoryObj = { 491 | args: { 492 | src: { 493 | obj: { 494 | k1: 123, 495 | k2: '123', 496 | k3: false 497 | } 498 | }, 499 | displaySize: true, 500 | CopyComponent: ({ onClick, className }) => ( 501 | 502 | 503 | 504 | ), 505 | CopiedComponent: ({ className, style }) => ( 506 | 507 | 508 | 509 | ) 510 | } 511 | } 512 | -------------------------------------------------------------------------------- /src/stories/share.ts: -------------------------------------------------------------------------------- 1 | export const argTypes = { 2 | src: { 3 | description: 'Array | Object' 4 | }, 5 | className: { 6 | control: 'text', 7 | description: 'String' 8 | }, 9 | style: { 10 | control: 'object', 11 | description: 'Object' 12 | }, 13 | dark: { 14 | control: 'boolean', 15 | description: 'Boolean', 16 | table: { 17 | defaultValue: { summary: false } 18 | } 19 | }, 20 | theme: { 21 | control: 'select', 22 | options: ['default', 'a11y', 'github', 'vscode', 'atom', 'winter-is-coming'], 23 | table: { 24 | defaultValue: { summary: false } 25 | } 26 | }, 27 | collapseStringsAfterLength: { 28 | control: 'number', 29 | description: 30 | 'When an integer value is assigned, strings longer than that length will be truncated and indicated by an ellipsis. To expand or collapse the string content, simply click on the string value.', 31 | table: { 32 | defaultValue: { summary: 99 } 33 | } 34 | }, 35 | collapseStringMode: { 36 | control: 'select', 37 | options: ['directly', 'word', 'address'], 38 | table: { 39 | defaultValue: { summary: 'word' } 40 | } 41 | }, 42 | collapseObjectsAfterLength: { 43 | control: 'number', 44 | description: 'When an integer value is assigned, the object and array will initially collapse.', 45 | table: { 46 | defaultValue: { summary: 99 } 47 | } 48 | }, 49 | collapsed: { 50 | description: 51 | 'When set to true, all nodes will be collapsed by default. Use an integer value to collapse at a specific depth. The collapsed also can be a function. ', 52 | table: { 53 | defaultValue: { summary: false } 54 | } 55 | }, 56 | enableClipboard: { 57 | control: 'boolean', 58 | description: 'Boolean', 59 | table: { 60 | defaultValue: { summary: false } 61 | } 62 | }, 63 | matchesURL: { 64 | control: 'boolean', 65 | description: 'Boolean', 66 | table: { 67 | defaultValue: { summary: false } 68 | } 69 | }, 70 | editable: { 71 | table: { 72 | defaultValue: { summary: false } 73 | }, 74 | description: 'When set to true, you can add, edit, or delete the property, and the actions will trigger onAdd, onEdit, or onDelete. Options is available.' 75 | }, 76 | onAdd: { 77 | description: `(params: { indexOrName: string | number, depth: number, src: any; parentType: 'object' | 'array' }) => void` 78 | }, 79 | onDelete: { 80 | description: `(params: { 81 | value: any, 82 | indexOrName: string | number, 83 | depth: number, 84 | src: any, 85 | parentType: 'object' | 'array', 86 | }) => void` 87 | }, 88 | onEdit: { 89 | description: `(params: { 90 | newValue: any, 91 | oldValue: any, 92 | depth: number, 93 | src: any, 94 | indexOrName: string | number, 95 | parentType: 'object' | 'array', 96 | }) => void` 97 | }, 98 | customizeNode: { 99 | description: 'Highly customize every node.' 100 | }, 101 | customizeCopy: { 102 | description: 'Customize copy behavior.' 103 | } 104 | } 105 | 106 | export const largeArray = [ 107 | 80, 108 | { 1: 1, 2: { 2: 2 } }, 109 | [1, [2, 2], [3, [3]]], 110 | 65, 111 | 85, 112 | 1, 113 | 0, 114 | 0, 115 | 0, 116 | 3, 117 | 184, 118 | 1, 119 | 0, 120 | 0, 121 | 0, 122 | 3, 123 | 13, 124 | 2, 125 | 121, 126 | 186, 127 | 6, 128 | 56, 129 | 151, 130 | 112, 131 | 160, 132 | 192, 133 | 103, 134 | 230, 135 | 223, 136 | 173, 137 | 213, 138 | 151, 139 | 104, 140 | 95, 141 | 90, 142 | 175, 143 | 137, 144 | 114, 145 | 220, 146 | 5, 147 | 166, 148 | 242, 149 | 114, 150 | 62, 151 | 120, 152 | 82, 153 | 231, 154 | 37, 155 | 102, 156 | 145, 157 | 116, 158 | 74, 159 | 228, 160 | 166, 161 | 36, 162 | 138, 163 | 186, 164 | 227, 165 | 23, 166 | 146, 167 | 212, 168 | 225, 169 | 12, 170 | 185, 171 | 71, 172 | 197, 173 | 15, 174 | 101, 175 | 203, 176 | 86, 177 | 2, 178 | 248, 179 | 12, 180 | 115, 181 | 233, 182 | 201, 183 | 42, 184 | 79, 185 | 226, 186 | 138, 187 | 44, 188 | 12, 189 | 1, 190 | 4, 191 | 157, 192 | 208, 193 | 184, 194 | 187, 195 | 127, 196 | 90, 197 | 250, 198 | 216, 199 | 242, 200 | 181, 201 | 250, 202 | 189, 203 | 98, 204 | 16, 205 | 235, 206 | 234, 207 | 149, 208 | 144, 209 | 123, 210 | 224, 211 | 41, 212 | 167, 213 | 81, 214 | 44, 215 | 171, 216 | 90, 217 | 47, 218 | 85, 219 | 152, 220 | 223, 221 | 136, 222 | 190, 223 | 1, 224 | 152, 225 | 0, 226 | 68, 227 | 157, 228 | 166, 229 | 231, 230 | 23, 231 | 165, 232 | 185, 233 | 184, 234 | 220, 235 | 182, 236 | 37, 237 | 60, 238 | 52, 239 | 146, 240 | 241, 241 | 170, 242 | 36, 243 | 164, 244 | 117, 245 | 93, 246 | 110, 247 | 3, 248 | 198, 249 | 12, 250 | 87, 251 | 249, 252 | 33, 253 | 18, 254 | 196, 255 | 1, 256 | 6, 257 | 226, 258 | 188, 259 | 138, 260 | 202, 261 | 169, 262 | 152, 263 | 161, 264 | 6, 265 | 36, 266 | 226, 267 | 186, 268 | 234, 269 | 50, 270 | 74, 271 | 8, 272 | 46, 273 | 231, 274 | 253, 275 | 157, 276 | 60, 277 | 41, 278 | 51, 279 | 98, 280 | 145, 281 | 149, 282 | 145, 283 | 8, 284 | 207, 285 | 26, 286 | 202, 287 | 192, 288 | 1, 289 | 41, 290 | 107, 291 | 95, 292 | 9, 293 | 167, 294 | 138, 295 | 95, 296 | 103, 297 | 203, 298 | 155, 299 | 75, 300 | 88, 301 | 163, 302 | 170, 303 | 8, 304 | 149, 305 | 93, 306 | 71, 307 | 210, 308 | 244, 309 | 125, 310 | 230, 311 | 249, 312 | 150, 313 | 103, 314 | 251, 315 | 134, 316 | 124, 317 | 149, 318 | 173, 319 | 106, 320 | 28, 321 | 0, 322 | 7, 323 | 243, 324 | 226, 325 | 158, 326 | 84, 327 | 94, 328 | 194, 329 | 7, 330 | 95, 331 | 243, 332 | 235, 333 | 143, 334 | 250, 335 | 119, 336 | 21, 337 | 129, 338 | 234, 339 | 93, 340 | 225, 341 | 206, 342 | 24, 343 | 134, 344 | 130, 345 | 131, 346 | 43, 347 | 108, 348 | 48, 349 | 241, 350 | 59, 351 | 139, 352 | 69, 353 | 79, 354 | 187, 355 | 51, 356 | 105, 357 | 199, 358 | 20, 359 | 222, 360 | 225, 361 | 123, 362 | 106, 363 | 82, 364 | 34, 365 | 60, 366 | 187, 367 | 60, 368 | 250, 369 | 222, 370 | 191, 371 | 119, 372 | 86, 373 | 123, 374 | 51, 375 | 46, 376 | 237, 377 | 214, 378 | 161, 379 | 238, 380 | 142, 381 | 88, 382 | 43, 383 | 36, 384 | 139, 385 | 3, 386 | 212, 387 | 1, 388 | 8, 389 | 123, 390 | 24, 391 | 130, 392 | 228, 393 | 156, 394 | 250, 395 | 90, 396 | 197, 397 | 208, 398 | 225, 399 | 102, 400 | 222, 401 | 101, 402 | 70, 403 | 61, 404 | 85, 405 | 229, 406 | 205, 407 | 172, 408 | 144, 409 | 26, 410 | 85, 411 | 84, 412 | 121, 413 | 229, 414 | 122, 415 | 78, 416 | 18, 417 | 7, 418 | 24, 419 | 33, 420 | 110, 421 | 58, 422 | 41, 423 | 161, 424 | 239, 425 | 202, 426 | 74, 427 | 43, 428 | 216, 429 | 59, 430 | 151, 431 | 116, 432 | 197, 433 | 17, 434 | 122, 435 | 185, 436 | 4, 437 | 16, 438 | 189, 439 | 224, 440 | 113, 441 | 9, 442 | 177, 443 | 139, 444 | 141, 445 | 189, 446 | 143, 447 | 255, 448 | 146, 449 | 90, 450 | 187, 451 | 182, 452 | 110, 453 | 1, 454 | 10, 455 | 94, 456 | 76, 457 | 53, 458 | 238, 459 | 86, 460 | 212, 461 | 41, 462 | 129, 463 | 7, 464 | 115, 465 | 109, 466 | 165, 467 | 226, 468 | 99, 469 | 212, 470 | 149, 471 | 45, 472 | 163, 473 | 37, 474 | 57, 475 | 86, 476 | 235, 477 | 246, 478 | 99, 479 | 111, 480 | 247, 481 | 150, 482 | 238, 483 | 119, 484 | 184, 485 | 252, 486 | 247, 487 | 46, 488 | 36, 489 | 139, 490 | 58, 491 | 178, 492 | 13, 493 | 179, 494 | 57, 495 | 16, 496 | 145, 497 | 29, 498 | 74, 499 | 222, 500 | 92, 501 | 185, 502 | 174, 503 | 15, 504 | 2, 505 | 25, 506 | 209, 507 | 241, 508 | 45, 509 | 81, 510 | 58, 511 | 65, 512 | 83, 513 | 214, 514 | 61, 515 | 155, 516 | 169, 517 | 115, 518 | 245, 519 | 0, 520 | 11, 521 | 211, 522 | 8, 523 | 189, 524 | 14, 525 | 182, 526 | 190, 527 | 141, 528 | 26, 529 | 120, 530 | 10, 531 | 30, 532 | 246, 533 | 214, 534 | 214, 535 | 34, 536 | 191, 537 | 166, 538 | 28, 539 | 29, 540 | 154, 541 | 31, 542 | 90, 543 | 48, 544 | 146, 545 | 74, 546 | 18, 547 | 177, 548 | 206, 549 | 66, 550 | 255, 551 | 10, 552 | 62, 553 | 108, 554 | 39, 555 | 176, 556 | 121, 557 | 177, 558 | 231, 559 | 137, 560 | 161, 561 | 34, 562 | 133, 563 | 187, 564 | 38, 565 | 187, 566 | 213, 567 | 9, 568 | 64, 569 | 150, 570 | 194, 571 | 156, 572 | 90, 573 | 150, 574 | 240, 575 | 203, 576 | 148, 577 | 123, 578 | 12, 579 | 237, 580 | 72, 581 | 161, 582 | 169, 583 | 227, 584 | 187, 585 | 1, 586 | 12, 587 | 131, 588 | 85, 589 | 178, 590 | 81, 591 | 89, 592 | 109, 593 | 167, 594 | 107, 595 | 249, 596 | 126, 597 | 154, 598 | 249, 599 | 39, 600 | 104, 601 | 38, 602 | 205, 603 | 192, 604 | 41, 605 | 175, 606 | 151, 607 | 96, 608 | 20, 609 | 8, 610 | 117, 611 | 58, 612 | 50, 613 | 175, 614 | 52, 615 | 57, 616 | 199, 617 | 104, 618 | 58, 619 | 81, 620 | 243, 621 | 55, 622 | 161, 623 | 163, 624 | 108, 625 | 21, 626 | 103, 627 | 141, 628 | 61, 629 | 253, 630 | 26, 631 | 136, 632 | 173, 633 | 48, 634 | 119, 635 | 223, 636 | 246, 637 | 111, 638 | 99, 639 | 213, 640 | 205, 641 | 233, 642 | 136, 643 | 1, 644 | 225, 645 | 183, 646 | 105, 647 | 95, 648 | 228, 649 | 42, 650 | 182, 651 | 1, 652 | 13, 653 | 22, 654 | 113, 655 | 162, 656 | 211, 657 | 40, 658 | 246, 659 | 12, 660 | 213, 661 | 30, 662 | 107, 663 | 124, 664 | 117, 665 | 9, 666 | 233, 667 | 211, 668 | 50, 669 | 61, 670 | 98, 671 | 87, 672 | 145, 673 | 76, 674 | 169, 675 | 58, 676 | 134, 677 | 16, 678 | 204, 679 | 246, 680 | 120, 681 | 40, 682 | 103, 683 | 72, 684 | 197, 685 | 77, 686 | 1, 687 | 161, 688 | 214, 689 | 193, 690 | 248, 691 | 238, 692 | 182, 693 | 78, 694 | 11, 695 | 236, 696 | 218, 697 | 157, 698 | 43, 699 | 4, 700 | 186, 701 | 2, 702 | 26, 703 | 121, 704 | 139, 705 | 144, 706 | 66, 707 | 26, 708 | 17, 709 | 94, 710 | 138, 711 | 109, 712 | 115, 713 | 191, 714 | 140, 715 | 33, 716 | 213, 717 | 0, 718 | 14, 719 | 156, 720 | 79, 721 | 100, 722 | 224, 723 | 40, 724 | 213, 725 | 31, 726 | 196, 727 | 39, 728 | 191, 729 | 235, 730 | 47, 731 | 231, 732 | 136, 733 | 145, 734 | 139, 735 | 9, 736 | 74, 737 | 238, 738 | 243, 739 | 133, 740 | 111, 741 | 84, 742 | 199, 743 | 150, 744 | 217, 745 | 235, 746 | 7, 747 | 250, 748 | 125, 749 | 61, 750 | 175, 751 | 89, 752 | 144, 753 | 43, 754 | 233, 755 | 236, 756 | 187, 757 | 26, 758 | 246, 759 | 99, 760 | 134, 761 | 51, 762 | 132, 763 | 220, 764 | 49, 765 | 87, 766 | 214, 767 | 76, 768 | 201, 769 | 139, 770 | 67, 771 | 205, 772 | 31, 773 | 229, 774 | 9, 775 | 113, 776 | 224, 777 | 89, 778 | 213, 779 | 99, 780 | 113, 781 | 71, 782 | 111, 783 | 0, 784 | 15, 785 | 33, 786 | 114, 787 | 28, 788 | 210, 789 | 244, 790 | 89, 791 | 240, 792 | 151, 793 | 165, 794 | 241, 795 | 212, 796 | 120, 797 | 2, 798 | 194, 799 | 233, 800 | 180, 801 | 93, 802 | 5, 803 | 238, 804 | 120, 805 | 15, 806 | 139, 807 | 224, 808 | 179, 809 | 187, 810 | 176, 811 | 185, 812 | 70, 813 | 52, 814 | 123, 815 | 25, 816 | 145, 817 | 33, 818 | 242, 819 | 192, 820 | 117, 821 | 55, 822 | 235, 823 | 250, 824 | 74, 825 | 149, 826 | 215, 827 | 111, 828 | 95, 829 | 244, 830 | 185, 831 | 242, 832 | 14, 833 | 159, 834 | 52, 835 | 238, 836 | 173, 837 | 97, 838 | 254, 839 | 208, 840 | 85, 841 | 204, 842 | 160, 843 | 42, 844 | 174, 845 | 22, 846 | 227, 847 | 13, 848 | 8, 849 | 1, 850 | 16, 851 | 68, 852 | 136, 853 | 108, 854 | 58, 855 | 224, 856 | 187, 857 | 244, 858 | 212, 859 | 255, 860 | 71, 861 | 34, 862 | 221, 863 | 27, 864 | 253, 865 | 138, 866 | 77, 867 | 202, 868 | 245, 869 | 102, 870 | 133, 871 | 211, 872 | 211, 873 | 174, 874 | 227, 875 | 2, 876 | 104, 877 | 62, 878 | 208, 879 | 61, 880 | 15, 881 | 196, 882 | 163, 883 | 112, 884 | 57, 885 | 166, 886 | 122, 887 | 110, 888 | 128, 889 | 17, 890 | 224, 891 | 33, 892 | 109, 893 | 12, 894 | 28, 895 | 74, 896 | 246, 897 | 150, 898 | 182, 899 | 255, 900 | 146, 901 | 249, 902 | 9, 903 | 28, 904 | 214, 905 | 123, 906 | 221, 907 | 120, 908 | 185, 909 | 141, 910 | 201, 911 | 44, 912 | 108, 913 | 34, 914 | 102, 915 | 0, 916 | 18, 917 | 108, 918 | 193, 919 | 84, 920 | 172, 921 | 81, 922 | 179, 923 | 225, 924 | 195, 925 | 131, 926 | 2, 927 | 0, 928 | 21, 929 | 142, 930 | 140, 931 | 50, 932 | 3, 933 | 223, 934 | 162, 935 | 195, 936 | 223, 937 | 208, 938 | 99, 939 | 252, 940 | 191, 941 | 50, 942 | 216, 943 | 13, 944 | 152, 945 | 236, 946 | 171, 947 | 172, 948 | 245, 949 | 18, 950 | 147, 951 | 156, 952 | 67, 953 | 192, 954 | 130, 955 | 107, 956 | 124, 957 | 117, 958 | 167, 959 | 120, 960 | 89, 961 | 201, 962 | 67, 963 | 141, 964 | 136, 965 | 32, 966 | 158, 967 | 64, 968 | 128, 969 | 249, 970 | 224, 971 | 22, 972 | 48, 973 | 175, 974 | 103, 975 | 224, 976 | 155, 977 | 5, 978 | 235, 979 | 207, 980 | 246, 981 | 1, 982 | 101, 983 | 164, 984 | 214, 985 | 118, 986 | 0, 987 | 0, 988 | 0, 989 | 0, 990 | 0, 991 | 26, 992 | 225, 993 | 1, 994 | 250, 995 | 237, 996 | 172, 997 | 88, 998 | 81, 999 | 227, 1000 | 43, 1001 | 155, 1002 | 35, 1003 | 181, 1004 | 249, 1005 | 65, 1006 | 26, 1007 | 140, 1008 | 43, 1009 | 172, 1010 | 74, 1011 | 174, 1012 | 62, 1013 | 212, 1014 | 221, 1015 | 123, 1016 | 129, 1017 | 29, 1018 | 209, 1019 | 167, 1020 | 46, 1021 | 164, 1022 | 170, 1023 | 113, 1024 | 0, 1025 | 0, 1026 | 0, 1027 | 0, 1028 | 2, 1029 | 41, 1030 | 109, 1031 | 38, 1032 | 1, 1033 | 65, 1034 | 85, 1035 | 87, 1036 | 86, 1037 | 0, 1038 | 0, 1039 | 0, 1040 | 0, 1041 | 0, 1042 | 7, 1043 | 48, 1044 | 177, 1045 | 76, 1046 | 0, 1047 | 0, 1048 | 39, 1049 | 16, 1050 | 103, 1051 | 123, 1052 | 175, 1053 | 16, 1054 | 218, 1055 | 153, 1056 | 59, 1057 | 20, 1058 | 203, 1059 | 105, 1060 | 228, 1061 | 123, 1062 | 46, 1063 | 106, 1064 | 212, 1065 | 223, 1066 | 151, 1067 | 222, 1068 | 99, 1069 | 254, 1070 | 3, 1071 | 0, 1072 | 85, 1073 | 0, 1074 | 35, 1075 | 86, 1076 | 175, 1077 | 149, 1078 | 41, 1079 | 161, 1080 | 6, 1081 | 77, 1082 | 65, 1083 | 227, 1084 | 45, 1085 | 97, 1086 | 126, 1087 | 44, 1088 | 225, 1089 | 220, 1090 | 165, 1091 | 115, 1092 | 58, 1093 | 250, 1094 | 144, 1095 | 29, 1096 | 171, 1097 | 169, 1098 | 226, 1099 | 182, 1100 | 141, 1101 | 238, 1102 | 93, 1103 | 83, 1104 | 236, 1105 | 249, 1106 | 0, 1107 | 0, 1108 | 0, 1109 | 0, 1110 | 17, 1111 | 227, 1112 | 150, 1113 | 179, 1114 | 0, 1115 | 0, 1116 | 0, 1117 | 0, 1118 | 0, 1119 | 5, 1120 | 0, 1121 | 129, 1122 | 255, 1123 | 255, 1124 | 255, 1125 | 248, 1126 | 0, 1127 | 0, 1128 | 0, 1129 | 0, 1130 | 101, 1131 | 164, 1132 | 214, 1133 | 118, 1134 | 0, 1135 | 0, 1136 | 0, 1137 | 0, 1138 | 101, 1139 | 164, 1140 | 214, 1141 | 117, 1142 | 0, 1143 | 0, 1144 | 0, 1145 | 0, 1146 | 17, 1147 | 201, 1148 | 8, 1149 | 84, 1150 | 0, 1151 | 0, 1152 | 0, 1153 | 0, 1154 | 0, 1155 | 4, 1156 | 47, 1157 | 79, 1158 | 10, 1159 | 54, 1160 | 90, 1161 | 117, 1162 | 73, 1163 | 30, 1164 | 131, 1165 | 22, 1166 | 242, 1167 | 0, 1168 | 165, 1169 | 35, 1170 | 44, 1171 | 86, 1172 | 5, 1173 | 90, 1174 | 115, 1175 | 157, 1176 | 217, 1177 | 149, 1178 | 77, 1179 | 197, 1180 | 4, 1181 | 182, 1182 | 88, 1183 | 183, 1184 | 242, 1185 | 234, 1186 | 63, 1187 | 42, 1188 | 48, 1189 | 41, 1190 | 224, 1191 | 189, 1192 | 89, 1193 | 90, 1194 | 239, 1195 | 224, 1196 | 65, 1197 | 86, 1198 | 24, 1199 | 255, 1200 | 22, 1201 | 53, 1202 | 122, 1203 | 134, 1204 | 59, 1205 | 16, 1206 | 136, 1207 | 202, 1208 | 164, 1209 | 149, 1210 | 192, 1211 | 39, 1212 | 9, 1213 | 255, 1214 | 236, 1215 | 225, 1216 | 223, 1217 | 168, 1218 | 52, 1219 | 245, 1220 | 29, 1221 | 162, 1222 | 237, 1223 | 64, 1224 | 118, 1225 | 214, 1226 | 185, 1227 | 55, 1228 | 180, 1229 | 148, 1230 | 75, 1231 | 225, 1232 | 6, 1233 | 172, 1234 | 83, 1235 | 182, 1236 | 51, 1237 | 7, 1238 | 30, 1239 | 149, 1240 | 254, 1241 | 106, 1242 | 192, 1243 | 190, 1244 | 190, 1245 | 210, 1246 | 26, 1247 | 82, 1248 | 55, 1249 | 170, 1250 | 114, 1251 | 167, 1252 | 239, 1253 | 162, 1254 | 50, 1255 | 8, 1256 | 5, 1257 | 205, 1258 | 42, 1259 | 26, 1260 | 75, 1261 | 62, 1262 | 25, 1263 | 231, 1264 | 118, 1265 | 33, 1266 | 11, 1267 | 230, 1268 | 205, 1269 | 211, 1270 | 188, 1271 | 63, 1272 | 108, 1273 | 109, 1274 | 196, 1275 | 180, 1276 | 204, 1277 | 33, 1278 | 157, 1279 | 219, 1280 | 96, 1281 | 125, 1282 | 163, 1283 | 248, 1284 | 66, 1285 | 114, 1286 | 19, 1287 | 51, 1288 | 30, 1289 | 86, 1290 | 120, 1291 | 134, 1292 | 104, 1293 | 56, 1294 | 150, 1295 | 91, 1296 | 57, 1297 | 7, 1298 | 189, 1299 | 180, 1300 | 47, 1301 | 22, 1302 | 113, 1303 | 27, 1304 | 232, 1305 | 251, 1306 | 21, 1307 | 137, 1308 | 9, 1309 | 138, 1310 | 1, 1311 | 34, 1312 | 243, 1313 | 166, 1314 | 138, 1315 | 10, 1316 | 87, 1317 | 100, 1318 | 191, 1319 | 76, 1320 | 186, 1321 | 1, 1322 | 125, 1323 | 233, 1324 | 5, 1325 | 40, 1326 | 136, 1327 | 151, 1328 | 152, 1329 | 33, 1330 | 111, 1331 | 163, 1332 | 151, 1333 | 137, 1334 | 129, 1335 | 233, 1336 | 204, 1337 | 121, 1338 | 168, 1339 | 205, 1340 | 19, 1341 | 38, 1342 | 187, 1343 | 5, 1344 | 218, 1345 | 26, 1346 | 166, 1347 | 80, 1348 | 231, 1349 | 173, 1350 | 125, 1351 | 181, 1352 | 64, 1353 | 61, 1354 | 25, 1355 | 241, 1356 | 220, 1357 | 70, 1358 | 252, 1359 | 0, 1360 | 85, 1361 | 0, 1362 | 35, 1363 | 215, 1364 | 49, 1365 | 81, 1366 | 19, 1367 | 245, 1368 | 177, 1369 | 211, 1370 | 186, 1371 | 122, 1372 | 131, 1373 | 96, 1374 | 76, 1375 | 68, 1376 | 185, 1377 | 77, 1378 | 121, 1379 | 244, 1380 | 253, 1381 | 105, 1382 | 175, 1383 | 119, 1384 | 248, 1385 | 4, 1386 | 252, 1387 | 127, 1388 | 146, 1389 | 10, 1390 | 109, 1391 | 198, 1392 | 87, 1393 | 68, 1394 | 0, 1395 | 0, 1396 | 0, 1397 | 0, 1398 | 8, 1399 | 80, 1400 | 101, 1401 | 172, 1402 | 0, 1403 | 0, 1404 | 0, 1405 | 0, 1406 | 0, 1407 | 2, 1408 | 52, 1409 | 101, 1410 | 255, 1411 | 255, 1412 | 255, 1413 | 248, 1414 | 0, 1415 | 0, 1416 | 0, 1417 | 0, 1418 | 101, 1419 | 164, 1420 | 214, 1421 | 118, 1422 | 0, 1423 | 0, 1424 | 0, 1425 | 0, 1426 | 101, 1427 | 164, 1428 | 214, 1429 | 117, 1430 | 0, 1431 | 0, 1432 | 0, 1433 | 0, 1434 | 8, 1435 | 63, 1436 | 95, 1437 | 21, 1438 | 0, 1439 | 0, 1440 | 0, 1441 | 0, 1442 | 0, 1443 | 2, 1444 | 119, 1445 | 209, 1446 | 10, 1447 | 161, 1448 | 248, 1449 | 146, 1450 | 171, 1451 | 227, 1452 | 175, 1453 | 169, 1454 | 205, 1455 | 54, 1456 | 92, 1457 | 186, 1458 | 2, 1459 | 102, 1460 | 211, 1461 | 1, 1462 | 167, 1463 | 46, 1464 | 10, 1465 | 7, 1466 | 36, 1467 | 26, 1468 | 152, 1469 | 85, 1470 | 185, 1471 | 126, 1472 | 91, 1473 | 143, 1474 | 115, 1475 | 22, 1476 | 145, 1477 | 254, 1478 | 90, 1479 | 227, 1480 | 75, 1481 | 109, 1482 | 86, 1483 | 213, 1484 | 8, 1485 | 95, 1486 | 199, 1487 | 6, 1488 | 217, 1489 | 169, 1490 | 240, 1491 | 165, 1492 | 134, 1493 | 181, 1494 | 72, 1495 | 203, 1496 | 142, 1497 | 42, 1498 | 137, 1499 | 177, 1500 | 26, 1501 | 141, 1502 | 240, 1503 | 137, 1504 | 153, 1505 | 206, 1506 | 206, 1507 | 245, 1508 | 29, 1509 | 162, 1510 | 237, 1511 | 64, 1512 | 118, 1513 | 214, 1514 | 185, 1515 | 55, 1516 | 180, 1517 | 148, 1518 | 75, 1519 | 225, 1520 | 6, 1521 | 172, 1522 | 83, 1523 | 182, 1524 | 51, 1525 | 7, 1526 | 30, 1527 | 149, 1528 | 254, 1529 | 106, 1530 | 192, 1531 | 190, 1532 | 190, 1533 | 210, 1534 | 26, 1535 | 82, 1536 | 55, 1537 | 170, 1538 | 114, 1539 | 167, 1540 | 239, 1541 | 162, 1542 | 50, 1543 | 8, 1544 | 5, 1545 | 205, 1546 | 42, 1547 | 26, 1548 | 75, 1549 | 62, 1550 | 25, 1551 | 231, 1552 | 118, 1553 | 33, 1554 | 11, 1555 | 230, 1556 | 205, 1557 | 211, 1558 | 188, 1559 | 63, 1560 | 108, 1561 | 109, 1562 | 196, 1563 | 180, 1564 | 204, 1565 | 33, 1566 | 157, 1567 | 219, 1568 | 96, 1569 | 125, 1570 | 163, 1571 | 248, 1572 | 66, 1573 | 114, 1574 | 19, 1575 | 51, 1576 | 30, 1577 | 86, 1578 | 120, 1579 | 134, 1580 | 104, 1581 | 56, 1582 | 150, 1583 | 91, 1584 | 57, 1585 | 7, 1586 | 189, 1587 | 180, 1588 | 47, 1589 | 22, 1590 | 113, 1591 | 27, 1592 | 232, 1593 | 251, 1594 | 21, 1595 | 137, 1596 | 9, 1597 | 138, 1598 | 1, 1599 | 34, 1600 | 243, 1601 | 166, 1602 | 138, 1603 | 10, 1604 | 87, 1605 | 100, 1606 | 191, 1607 | 76, 1608 | 186, 1609 | 1, 1610 | 125, 1611 | 233, 1612 | 5, 1613 | 40, 1614 | 136, 1615 | 151, 1616 | 152, 1617 | 33, 1618 | 111, 1619 | 163, 1620 | 151, 1621 | 137, 1622 | 129, 1623 | 233, 1624 | 204, 1625 | 121, 1626 | 168, 1627 | 205, 1628 | 19, 1629 | 38, 1630 | 187, 1631 | 5, 1632 | 218, 1633 | 26, 1634 | 166, 1635 | 80, 1636 | 231, 1637 | 173, 1638 | 125, 1639 | 181, 1640 | 64, 1641 | 61, 1642 | 25, 1643 | 241, 1644 | 220, 1645 | 70, 1646 | 252, 1647 | 0, 1648 | 85, 1649 | 0, 1650 | 229, 1651 | 178, 1652 | 116, 1653 | 178, 1654 | 97, 1655 | 17, 1656 | 67, 1657 | 223, 1658 | 5, 1659 | 93, 1660 | 110, 1661 | 124, 1662 | 216, 1663 | 217, 1664 | 63, 1665 | 225, 1666 | 150, 1667 | 23, 1668 | 22, 1669 | 188, 1670 | 212, 1671 | 220, 1672 | 161, 1673 | 202, 1674 | 216, 1675 | 122, 1676 | 131, 1677 | 188, 1678 | 30, 1679 | 120, 1680 | 193, 1681 | 239, 1682 | 0, 1683 | 0, 1684 | 0, 1685 | 0, 1686 | 0, 1687 | 211, 1688 | 102, 1689 | 126, 1690 | 0, 1691 | 0, 1692 | 0, 1693 | 0, 1694 | 0, 1695 | 0, 1696 | 81, 1697 | 70, 1698 | 255, 1699 | 255, 1700 | 255, 1701 | 248, 1702 | 0, 1703 | 0, 1704 | 0, 1705 | 0, 1706 | 101, 1707 | 164, 1708 | 214, 1709 | 118, 1710 | 0, 1711 | 0, 1712 | 0, 1713 | 0, 1714 | 101, 1715 | 164, 1716 | 214, 1717 | 117, 1718 | 0, 1719 | 0, 1720 | 0, 1721 | 0, 1722 | 0, 1723 | 209, 1724 | 139, 1725 | 204, 1726 | 0, 1727 | 0, 1728 | 0, 1729 | 0, 1730 | 0, 1731 | 0, 1732 | 96, 1733 | 149, 1734 | 10, 1735 | 62, 1736 | 205, 1737 | 57, 1738 | 44, 1739 | 196, 1740 | 138, 1741 | 88, 1742 | 133, 1743 | 149, 1744 | 104, 1745 | 117, 1746 | 136, 1747 | 186, 1748 | 96, 1749 | 134, 1750 | 179, 1751 | 113, 1752 | 63, 1753 | 17, 1754 | 85, 1755 | 189, 1756 | 83, 1757 | 37, 1758 | 152, 1759 | 240, 1760 | 134, 1761 | 20, 1762 | 35, 1763 | 121, 1764 | 186, 1765 | 18, 1766 | 20, 1767 | 29, 1768 | 151, 1769 | 145, 1770 | 128, 1771 | 223, 1772 | 94, 1773 | 86, 1774 | 115, 1775 | 69, 1776 | 94, 1777 | 149, 1778 | 125, 1779 | 204, 1780 | 127, 1781 | 169, 1782 | 100, 1783 | 3, 1784 | 245, 1785 | 207, 1786 | 96, 1787 | 108, 1788 | 201, 1789 | 211, 1790 | 142, 1791 | 212, 1792 | 199, 1793 | 162, 1794 | 124, 1795 | 184, 1796 | 247, 1797 | 189, 1798 | 70, 1799 | 48, 1800 | 68, 1801 | 237, 1802 | 226, 1803 | 173, 1804 | 185, 1805 | 89, 1806 | 156, 1807 | 206, 1808 | 175, 1809 | 110, 1810 | 124, 1811 | 87, 1812 | 236, 1813 | 119, 1814 | 94, 1815 | 1, 1816 | 123, 1817 | 213, 1818 | 151, 1819 | 224, 1820 | 235, 1821 | 40, 1822 | 250, 1823 | 126, 1824 | 237, 1825 | 49, 1826 | 45, 1827 | 62, 1828 | 253, 1829 | 237, 1830 | 210, 1831 | 241, 1832 | 105, 1833 | 61, 1834 | 20, 1835 | 143, 1836 | 60, 1837 | 15, 1838 | 223, 1839 | 245, 1840 | 76, 1841 | 16, 1842 | 5, 1843 | 239, 1844 | 174, 1845 | 171, 1846 | 17, 1847 | 109, 1848 | 109, 1849 | 133, 1850 | 211, 1851 | 33, 1852 | 191, 1853 | 55, 1854 | 118, 1855 | 234, 1856 | 62, 1857 | 174, 1858 | 91, 1859 | 116, 1860 | 191, 1861 | 120, 1862 | 177, 1863 | 136, 1864 | 168, 1865 | 7, 1866 | 179, 1867 | 144, 1868 | 172, 1869 | 193, 1870 | 10, 1871 | 147, 1872 | 48, 1873 | 210, 1874 | 119, 1875 | 200, 1876 | 51, 1877 | 96, 1878 | 50, 1879 | 193, 1880 | 173, 1881 | 73, 1882 | 115, 1883 | 253, 1884 | 163, 1885 | 102, 1886 | 6, 1887 | 92, 1888 | 4, 1889 | 120, 1890 | 17, 1891 | 65, 1892 | 115, 1893 | 253, 1894 | 103, 1895 | 67, 1896 | 35, 1897 | 245, 1898 | 187, 1899 | 83, 1900 | 59, 1901 | 183, 1902 | 69, 1903 | 77, 1904 | 240, 1905 | 152, 1906 | 215, 1907 | 75, 1908 | 10, 1909 | 159, 1910 | 61, 1911 | 21, 1912 | 46, 1913 | 141, 1914 | 255, 1915 | 205, 1916 | 19, 1917 | 38, 1918 | 187, 1919 | 5, 1920 | 218, 1921 | 26, 1922 | 166, 1923 | 80, 1924 | 231, 1925 | 173, 1926 | 125, 1927 | 181, 1928 | 64, 1929 | 61, 1930 | 25, 1931 | 241, 1932 | 220, 1933 | 70, 1934 | 252 1935 | ] 1936 | -------------------------------------------------------------------------------- /src/stories/theme.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Meta, StoryObj } from '@storybook/react' 3 | import JsonView from '../index' 4 | import '../dark.css' 5 | import { argTypes } from './share' 6 | 7 | type TYPE_FC = typeof JsonView 8 | 9 | export default { 10 | title: 'Themes', 11 | component: JsonView, 12 | 13 | argTypes, 14 | args: { 15 | enableClipboard: true, 16 | editable: true, 17 | src: { 18 | string: 'string', 19 | longString: 'long string long string long string long string long string long string', 20 | number: 123456, 21 | boolean: false, 22 | null: null, 23 | func: function () { 24 | console.log('Hello World') 25 | }, 26 | Symbol: Symbol('JSON View'), 27 | obj: { 28 | k1: 123, 29 | k2: '123', 30 | k3: false 31 | }, 32 | arr: ['string', 123456, false, null] 33 | } 34 | } 35 | } as Meta 36 | 37 | export const Default: StoryObj = { 38 | args: { 39 | theme: 'default' 40 | }, 41 | decorators: [ 42 | Story => ( 43 |
44 |
45 | 46 |
47 |
48 | ) 49 | ] 50 | } 51 | export const Default_Dark: StoryObj = { 52 | args: { 53 | theme: 'default', 54 | dark: true 55 | }, 56 | decorators: [ 57 | Story => ( 58 |
59 |
60 | 61 |
62 |
63 | ) 64 | ] 65 | } 66 | 67 | export const Accessibility: StoryObj = { 68 | args: { 69 | theme: 'a11y' 70 | }, 71 | decorators: [ 72 | Story => ( 73 |
74 |
75 | 76 |
77 |
78 | ) 79 | ] 80 | } 81 | export const Accessibility_Dark: StoryObj = { 82 | args: { 83 | theme: 'a11y', 84 | dark: true 85 | }, 86 | decorators: [ 87 | Story => ( 88 |
89 |
90 | 91 |
92 |
93 | ) 94 | ] 95 | } 96 | 97 | export const Github: StoryObj = { 98 | args: { 99 | theme: 'github' 100 | }, 101 | decorators: [ 102 | Story => ( 103 |
104 |
105 | 106 |
107 |
108 | ) 109 | ] 110 | } 111 | export const Github_Dark: StoryObj = { 112 | args: { 113 | theme: 'github', 114 | dark: true 115 | }, 116 | decorators: [ 117 | Story => ( 118 |
119 |
120 | 121 |
122 |
123 | ) 124 | ] 125 | } 126 | 127 | export const Vscode: StoryObj = { 128 | args: { 129 | theme: 'vscode' 130 | }, 131 | decorators: [ 132 | Story => ( 133 |
134 |
135 | 136 |
137 |
138 | ) 139 | ] 140 | } 141 | export const Vscode_Dark: StoryObj = { 142 | args: { 143 | theme: 'vscode', 144 | dark: true 145 | }, 146 | decorators: [ 147 | Story => ( 148 |
149 |
150 | 151 |
152 |
153 | ) 154 | ] 155 | } 156 | 157 | export const Atom: StoryObj = { 158 | args: { 159 | theme: 'atom' 160 | }, 161 | decorators: [ 162 | Story => ( 163 |
164 |
165 | 166 |
167 |
168 | ) 169 | ] 170 | } 171 | export const Atom_Dark: StoryObj = { 172 | args: { 173 | theme: 'atom', 174 | dark: true 175 | }, 176 | decorators: [ 177 | Story => ( 178 |
179 |
180 | 181 |
182 |
183 | ) 184 | ] 185 | } 186 | 187 | export const Winter_is_Coming: StoryObj = { 188 | args: { 189 | theme: 'winter-is-coming' 190 | }, 191 | decorators: [ 192 | Story => ( 193 |
194 |
195 | 196 |
197 |
198 | ) 199 | ] 200 | } 201 | export const Winter_is_Coming_Dark: StoryObj = { 202 | args: { 203 | theme: 'winter-is-coming', 204 | dark: true 205 | }, 206 | decorators: [ 207 | Story => ( 208 |
209 |
210 | 211 |
212 |
213 | ) 214 | ] 215 | } 216 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .json-view { 2 | display: block; 3 | color: #4d4d4d; 4 | text-align: left; 5 | --json-property: #009033; 6 | --json-index: #676dff; 7 | --json-number: #676dff; 8 | --json-string: #b2762e; 9 | --json-boolean: #dc155e; 10 | --json-null: #dc155e; 11 | } 12 | .json-view .json-view--property { 13 | color: var(--json-property); 14 | } 15 | .json-view .json-view--index { 16 | color: var(--json-index); 17 | } 18 | .json-view .json-view--number { 19 | color: var(--json-number); 20 | } 21 | .json-view .json-view--string { 22 | color: var(--json-string); 23 | } 24 | .json-view .json-view--boolean { 25 | color: var(--json-boolean); 26 | } 27 | .json-view .json-view--null { 28 | color: var(--json-null); 29 | } 30 | 31 | .json-view .jv-indent { 32 | padding-left: 1em; 33 | } 34 | .json-view .jv-chevron { 35 | display: inline-block; 36 | vertical-align: -20%; 37 | cursor: pointer; 38 | opacity: 0.4; 39 | width: 1em; 40 | height: 1em; 41 | } 42 | :is(.json-view .jv-chevron:hover, .json-view .jv-size:hover + .jv-chevron) { 43 | opacity: 0.8; 44 | } 45 | .json-view .jv-size { 46 | cursor: pointer; 47 | opacity: 0.4; 48 | font-size: 0.875em; 49 | font-style: italic; 50 | margin-left: 0.5em; 51 | vertical-align: -5%; 52 | line-height: 1; 53 | } 54 | 55 | .json-view :is(.json-view--copy, .json-view--edit), 56 | .json-view .json-view--link svg { 57 | display: none; 58 | width: 1em; 59 | height: 1em; 60 | margin-left: 0.25em; 61 | cursor: pointer; 62 | } 63 | 64 | .json-view .json-view--input { 65 | width: 120px; 66 | margin-left: 0.25em; 67 | border-radius: 4px; 68 | border: 1px solid currentColor; 69 | padding: 0px 4px; 70 | font-size: 87.5%; 71 | line-height: 1.25; 72 | background: transparent; 73 | } 74 | .json-view .json-view--deleting { 75 | outline: 1px solid #da0000; 76 | background-color: #da000011; 77 | text-decoration-line: line-through; 78 | } 79 | 80 | :is(.json-view:hover, .json-view--pair:hover) > :is(.json-view--copy, .json-view--edit), 81 | :is(.json-view:hover, .json-view--pair:hover) > .json-view--link svg { 82 | display: inline-block; 83 | } 84 | 85 | .json-view .jv-button { 86 | background: transparent; 87 | outline: none; 88 | border: none; 89 | cursor: pointer; 90 | color: inherit; 91 | } 92 | .json-view .cursor-pointer { 93 | cursor: pointer; 94 | } 95 | 96 | .json-view svg { 97 | vertical-align: -10%; 98 | } 99 | .jv-size-chevron ~ svg { 100 | vertical-align: -16%; 101 | } 102 | 103 | /* Themes */ 104 | .json-view_a11y { 105 | color: #545454; 106 | --json-property: #aa5d00; 107 | --json-index: #007299; 108 | --json-number: #007299; 109 | --json-string: #008000; 110 | --json-boolean: #d91e18; 111 | --json-null: #d91e18; 112 | } 113 | .json-view_github { 114 | color: #005cc5; 115 | --json-property: #005cc5; 116 | --json-index: #005cc5; 117 | --json-number: #005cc5; 118 | --json-string: #032f62; 119 | --json-boolean: #005cc5; 120 | --json-null: #005cc5; 121 | } 122 | .json-view_vscode { 123 | color: #005cc5; 124 | --json-property: #0451a5; 125 | --json-index: #0000ff; 126 | --json-number: #0000ff; 127 | --json-string: #a31515; 128 | --json-boolean: #0000ff; 129 | --json-null: #0000ff; 130 | } 131 | .json-view_atom { 132 | color: #383a42; 133 | --json-property: #e45649; 134 | --json-index: #986801; 135 | --json-number: #986801; 136 | --json-string: #50a14f; 137 | --json-boolean: #0184bc; 138 | --json-null: #0184bc; 139 | } 140 | .json-view_winter-is-coming { 141 | color: #0431fa; 142 | --json-property: #3a9685; 143 | --json-index: #ae408b; 144 | --json-number: #ae408b; 145 | --json-string: #8123a9; 146 | --json-boolean: #0184bc; 147 | --json-null: #0184bc; 148 | } 149 | -------------------------------------------------------------------------------- /src/svgs/add-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | -------------------------------------------------------------------------------- /src/svgs/add-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | -------------------------------------------------------------------------------- /src/svgs/angle-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/svgs/cancel.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | -------------------------------------------------------------------------------- /src/svgs/copied.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | -------------------------------------------------------------------------------- /src/svgs/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/svgs/custom-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /src/svgs/custom-copied.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /src/svgs/custom-copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svgs/done.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | -------------------------------------------------------------------------------- /src/svgs/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/svgs/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/svgs/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export declare type Collapsed = 2 | | undefined 3 | | number 4 | | boolean 5 | | ((params: { node: Record | Array; indexOrName: number | string | undefined; depth: number; size: number }) => boolean | void) 6 | 7 | export type DisplaySize = undefined | number | boolean | 'collapsed' | 'expanded' 8 | 9 | export declare type Editable = 10 | | boolean 11 | | { 12 | add?: boolean 13 | edit?: boolean 14 | delete?: boolean 15 | } 16 | 17 | export declare type CustomizeOptions = { 18 | add?: boolean 19 | edit?: boolean 20 | delete?: boolean 21 | enableClipboard?: boolean 22 | matchesURL?: boolean 23 | collapsed?: boolean 24 | className?: string 25 | } 26 | export declare type CustomizeNode = (params: { 27 | node: any 28 | indexOrName: number | string | undefined 29 | depth: number 30 | }) => CustomizeOptions | React.FC | React.Component | React.ReactElement | undefined 31 | 32 | export type CustomizeCollapseStringUI = ((str_show: string, truncated: boolean) => JSX.Element | string) | string 33 | 34 | export type NodeMeta = { depth: number; indexOrName?: number | string; parent?: Record | Array; parentPath: string[], currentPath: string[] } 35 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Collapsed, CustomizeOptions, DisplaySize, Editable } from './types' 2 | import copy from 'copy-to-clipboard' 3 | 4 | export function isObject(node: any): node is Record { 5 | return Object.prototype.toString.call(node) === '[object Object]' 6 | } 7 | 8 | export function objectSize(node: Record | Array) { 9 | return Array.isArray(node) ? node.length : isObject(node) ? Object.keys(node).length : 0 10 | } 11 | 12 | export function stringifyForCopying(node: any, space?: string | number | undefined) { 13 | // return single string nodes without quotes 14 | if (typeof node === 'string') { 15 | return node 16 | } 17 | 18 | try { 19 | return JSON.stringify( 20 | node, 21 | (key, value) => { 22 | switch (typeof value) { 23 | case 'bigint': 24 | return String(value) + 'n' 25 | case 'number': 26 | case 'boolean': 27 | case 'object': 28 | case 'string': 29 | return value 30 | default: 31 | return String(value) 32 | } 33 | }, 34 | space 35 | ) 36 | } catch (error: any) { 37 | return `${error.name}: ${error.message}` || 'JSON.stringify failed' 38 | } 39 | } 40 | 41 | export async function writeClipboard(value: string) { 42 | try { 43 | await navigator.clipboard.writeText(value) 44 | } catch (err) { 45 | copy(value) 46 | } 47 | } 48 | 49 | export function isCollapsed( 50 | node: Record | Array, 51 | depth: number, 52 | indexOrName: number | string | undefined, 53 | collapsed: Collapsed, 54 | collapseObjectsAfterLength: number, 55 | customOptions?: CustomizeOptions 56 | ): boolean { 57 | if (customOptions && customOptions.collapsed !== undefined) return !!customOptions.collapsed 58 | if (typeof collapsed === 'boolean') return collapsed 59 | if (typeof collapsed === 'number' && depth > collapsed) return true 60 | 61 | const size = objectSize(node) 62 | 63 | if (typeof collapsed === 'function') { 64 | const result = safeCall(collapsed, [{ node, depth, indexOrName, size }]) 65 | if (typeof result === 'boolean') return result 66 | } 67 | 68 | if (Array.isArray(node) && size > collapseObjectsAfterLength) return true 69 | if (isObject(node) && size > collapseObjectsAfterLength) return true 70 | return false 71 | } 72 | export function isCollapsed_largeArray( 73 | node: Record | Array, 74 | depth: number, 75 | indexOrName: number | string | undefined, 76 | collapsed: Collapsed, 77 | collapseObjectsAfterLength: number, 78 | customOptions?: CustomizeOptions 79 | ): boolean { 80 | if (customOptions && customOptions.collapsed !== undefined) return !!customOptions.collapsed 81 | if (typeof collapsed === 'boolean') return collapsed 82 | if (typeof collapsed === 'number' && depth > collapsed) return true 83 | 84 | const size = Math.ceil(node.length / 100) 85 | 86 | if (typeof collapsed === 'function') { 87 | const result = safeCall(collapsed, [{ node, depth, indexOrName, size }]) 88 | if (typeof result === 'boolean') return result 89 | } 90 | 91 | if (Array.isArray(node) && size > collapseObjectsAfterLength) return true 92 | if (isObject(node) && size > collapseObjectsAfterLength) return true 93 | return false 94 | } 95 | 96 | export function ifDisplay(displaySize: DisplaySize, depth: number, fold: boolean) { 97 | if (typeof displaySize === 'boolean') return displaySize 98 | if (typeof displaySize === 'number' && depth > displaySize) return true 99 | if (displaySize === 'collapsed' && fold) return true 100 | if (displaySize === 'expanded' && !fold) return true 101 | 102 | return false 103 | } 104 | 105 | export function safeCall any>(func: T, params: Parameters) { 106 | try { 107 | return func(...params) 108 | } catch (event) { 109 | reportError(event) 110 | } 111 | } 112 | 113 | export function editableAdd(editable: Editable) { 114 | if (editable === true) return true 115 | if (isObject(editable) && (editable as { add: boolean }).add === true) return true 116 | } 117 | export function editableEdit(editable: Editable) { 118 | if (editable === true) return true 119 | if (isObject(editable) && (editable as { edit: boolean }).edit === true) return true 120 | } 121 | export function editableDelete(editable: Editable) { 122 | if (editable === true) return true 123 | if (isObject(editable) && (editable as { delete: boolean }).delete === true) return true 124 | } 125 | 126 | function isClassComponent(component: any) { 127 | return typeof component === 'function' && !!component.prototype?.isReactComponent 128 | } 129 | export function isReactComponent(component: any): component is (new () => React.Component) | React.FC { 130 | return typeof component === 'function' 131 | } 132 | 133 | export function customAdd(customOptions?: CustomizeOptions) { 134 | return !customOptions || customOptions.add === undefined || !!customOptions.add 135 | } 136 | export function customEdit(customOptions?: CustomizeOptions) { 137 | return !customOptions || customOptions.edit === undefined || !!customOptions.edit 138 | } 139 | export function customDelete(customOptions?: CustomizeOptions) { 140 | return !customOptions || customOptions.delete === undefined || !!customOptions.delete 141 | } 142 | export function customCopy(customOptions?: CustomizeOptions) { 143 | return !customOptions || customOptions.enableClipboard === undefined || !!customOptions.enableClipboard 144 | } 145 | export function customMatchesURL(customOptions?: CustomizeOptions) { 146 | return !customOptions || customOptions.matchesURL === undefined || !!customOptions.matchesURL 147 | } 148 | 149 | export function resolveEvalFailedNewValue(type: string, value: string) { 150 | if (type === 'string') { 151 | return value.trim().replace(/^\"([\s\S]+?)\"$/, '$1') 152 | } 153 | return value 154 | } 155 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{tsx,css}'] 4 | } 5 | -------------------------------------------------------------------------------- /theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYsuni/react18-json-view/b8513baf7ee215ee6105aa05a6c6c71c6515ee4e/theme.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ES6", 5 | "skipLibCheck": true, 6 | "esModuleInterop": false, 7 | "allowSyntheticDefaultImports": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "module": "ESNext", 10 | "moduleResolution": "Node", 11 | "isolatedModules": true, 12 | "jsx": "react-jsx", 13 | "sourceMap": true, 14 | "declaration": true, 15 | "baseUrl": ".", 16 | "declarationDir": "./dist" 17 | }, 18 | "include": ["./src"], 19 | "exclude": ["./node_modules", "./test", "**/*.stories.tsx"] 20 | } 21 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": ["//"], 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**", ".next/**"] 8 | }, 9 | "dev": { 10 | "cache": false 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }] 3 | } 4 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /website/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | README.md 4 | .next 5 | pnpm-lock.yaml 6 | dist 7 | storybook-static -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /website/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | export default React.FC> 3 | } 4 | 5 | declare type NullableObject = Record | null 6 | declare type NullableArray = Record[] | null 7 | declare type Nullable = T | null 8 | -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | webpack: config => { 4 | config.module.rules.push({ 5 | test: /\.svg$/i, 6 | use: [{ loader: '@svgr/webpack', options: { dimensions: false } }] 7 | }) 8 | return config 9 | } 10 | } 11 | 12 | module.exports = nextConfig 13 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "format": "prettier --write ." 11 | }, 12 | "dependencies": { 13 | "@types/node": "20.5.9", 14 | "@types/react": "18.2.21", 15 | "@types/react-dom": "18.2.7", 16 | "autoprefixer": "10.4.15", 17 | "clsx": "^2.0.0", 18 | "highlight.js": "^11.8.0", 19 | "hljs": "^6.2.3", 20 | "next": "13.4.19", 21 | "postcss": "8.4.29", 22 | "prettier": "^2.8.2", 23 | "prettier-plugin-tailwindcss": "^0.2.8", 24 | "react": "18.2.0", 25 | "react-dom": "18.2.0", 26 | "react18-json-view": "workspace:*", 27 | "tailwindcss": "3.3.3", 28 | "typescript": "5.2.2", 29 | "zustand": "^4.4.1" 30 | }, 31 | "devDependencies": { 32 | "@svgr/webpack": "^8.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /website/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | jsxSingleQuote: true, 3 | jsxBracketSameLine: true, 4 | printWidth: 120, 5 | singleQuote: true, 6 | trailingComma: 'none', 7 | useTabs: true, 8 | tabWidth: 2, 9 | semi: false, 10 | arrowParens: 'avoid', 11 | bracketSameLine: true, 12 | plugins: [require('prettier-plugin-tailwindcss')] 13 | } 14 | -------------------------------------------------------------------------------- /website/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /website/public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YYsuni/react18-json-view/b8513baf7ee215ee6105aa05a6c6c71c6515ee4e/website/public/og.png -------------------------------------------------------------------------------- /website/src/app/footer.tsx: -------------------------------------------------------------------------------- 1 | export default function Footer() { 2 | return ( 3 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /website/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | @apply h-full; 8 | } 9 | 10 | body { 11 | background-image: linear-gradient(to bottom, #eef4f6 0, transparent 400px); 12 | background-repeat: no-repeat; 13 | @apply bg-[#fefefe] text-[#383a42] dark:bg-[#272a34] dark:text-[#eee]; 14 | } 15 | 16 | .dark body { 17 | background-image: none; 18 | } 19 | -------------------------------------------------------------------------------- /website/src/app/head.tsx: -------------------------------------------------------------------------------- 1 | export default function Head() { 2 | return ( 3 | <> 4 | JsonView 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /website/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Footer from './footer' 2 | import './globals.css' 3 | import Head from './head' 4 | 5 | export default function RootLayout({ children }: { children: React.ReactNode }) { 6 | return ( 7 | 8 | 9 | 10 |