Skip to content

Commit e300594

Browse files
committed
rc1
1 parent 7b9e519 commit e300594

12 files changed

Lines changed: 432 additions & 334 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Deploy to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [master]
6+
7+
permissions:
8+
contents: read
9+
pages: write
10+
id-token: write
11+
12+
concurrency:
13+
group: "pages"
14+
cancel-in-progress: false
15+
16+
jobs:
17+
deploy:
18+
environment:
19+
name: github-pages
20+
url: ${{ steps.deployment.outputs.page_url }}
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- uses: actions/setup-node@v4
26+
with:
27+
node-version: '20'
28+
cache: 'npm'
29+
30+
- run: npm ci
31+
32+
- name: Copy sample data files
33+
run: |
34+
mkdir -p public/data
35+
cp data/*.json public/data/
36+
37+
- run: npm run build
38+
39+
- uses: actions/configure-pages@v4
40+
41+
- uses: actions/upload-pages-artifact@v3
42+
with:
43+
path: ./dist
44+
45+
- id: deployment
46+
uses: actions/deploy-pages@v4

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 metaory
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.

README.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# json-diff-viewer
2+
3+
Vanilla JS web component for side-by-side JSON diff visualization.
4+
5+
## Features
6+
7+
- Deep nested JSON comparison
8+
- Side-by-side synchronized scrolling
9+
- Collapsible nodes (synced between panels)
10+
- Diff indicators bubble up to parent nodes
11+
- Stats summary (added/removed/modified/type-changed)
12+
- Syntax highlighting
13+
- Zero dependencies
14+
- Shadow DOM encapsulation
15+
16+
## Install
17+
18+
```bash
19+
npm i json-diff-viewer-component
20+
```
21+
22+
## Usage
23+
24+
### ES Module
25+
26+
```js
27+
import 'json-diff-viewer-component'
28+
29+
const viewer = document.querySelector('json-diff-viewer')
30+
viewer.setData(leftObj, rightObj)
31+
```
32+
33+
### HTML Attributes
34+
35+
```html
36+
<json-diff-viewer
37+
left='{"name":"foo"}'
38+
right='{"name":"bar"}'
39+
></json-diff-viewer>
40+
```
41+
42+
### Properties
43+
44+
```js
45+
viewer.left = { name: 'foo' }
46+
viewer.right = { name: 'bar' }
47+
```
48+
49+
### Method
50+
51+
```js
52+
viewer.setData(leftObj, rightObj)
53+
```
54+
55+
## Diff Types
56+
57+
| Type | Color | Description |
58+
|------|-------|-------------|
59+
| Added | Green | Key exists only in right |
60+
| Removed | Red | Key exists only in left |
61+
| Modified | Yellow | Value changed |
62+
| Type Changed | Orange | Type mismatch (e.g. number → string) |
63+
64+
## Styling
65+
66+
Override CSS custom properties:
67+
68+
```css
69+
json-diff-viewer {
70+
/* Diff colors */
71+
--added: #22c55e;
72+
--removed: #ef4444;
73+
--modified: #eab308;
74+
--type-changed: #f97316;
75+
--unchanged: #71717a;
76+
77+
/* Background */
78+
--bg: #18181b;
79+
--bg-panel: #27272a;
80+
--border: #3f3f46;
81+
82+
/* Text */
83+
--text: #fafafa;
84+
--text-dim: #a1a1aa;
85+
86+
/* Syntax */
87+
--key: #38bdf8;
88+
--string: #a78bfa;
89+
--number: #34d399;
90+
--boolean: #fb923c;
91+
--null: #f472b6;
92+
--bracket: #71717a;
93+
94+
/* Layout */
95+
--radius: 12px;
96+
--font: 'JetBrains Mono', monospace;
97+
}
98+
```
99+
100+
### Light Theme
101+
102+
```css
103+
json-diff-viewer {
104+
--bg: #fafafa;
105+
--bg-panel: #ffffff;
106+
--border: #e4e4e7;
107+
--text: #18181b;
108+
--text-dim: #71717a;
109+
--key: #0284c7;
110+
--string: #7c3aed;
111+
--number: #059669;
112+
}
113+
```
114+
115+
### Sizing
116+
117+
```css
118+
json-diff-viewer {
119+
height: 600px;
120+
border-radius: 16px;
121+
}
122+
```
123+
124+
## Dev
125+
126+
```bash
127+
npm run dev # start dev server
128+
npm run build # build for production
129+
```
130+
131+
## License
132+
133+
MIT

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</head>
88
<body>
99
<div id="app">
10-
<h1>JSON Diff Viewer</h1>
10+
<h1>JSON Diff Viewer<a href="https://github.com/metaory/json-diff-viewer-component" target="_blank" rel="noopener noreferrer"><img src="/github.svg" alt="GitHub" class="github-logo"></a></h1>
1111
<json-diff-viewer></json-diff-viewer>
1212
</div>
1313
<script type="module" src="/src/main.js"></script>

package-lock.json

Lines changed: 23 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
11
{
22
"name": "json-diff-viewer-component",
3-
"private": true,
4-
"version": "0.0.0",
3+
"version": "1.0.0",
54
"type": "module",
5+
"description": "Vanilla JS web component for side-by-side JSON diff visualization",
6+
"keywords": [
7+
"json",
8+
"diff",
9+
"viewer",
10+
"web-component",
11+
"custom-element",
12+
"comparison"
13+
],
14+
"license": "MIT",
15+
"exports": {
16+
".": "./src/lib/viewer.js"
17+
},
18+
"files": [
19+
"src/lib/**",
20+
"README.md",
21+
"LICENSE"
22+
],
623
"scripts": {
724
"dev": "vite",
825
"build": "vite build",
926
"preview": "vite preview"
1027
},
1128
"devDependencies": {
29+
"@fontsource/bungee": "^5.2.7",
1230
"vite": "^7.2.4"
1331
}
1432
}

public/github.svg

Lines changed: 6 additions & 0 deletions
Loading

src/lib/diff.js

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,38 @@
1-
const TYPES = { UNCHANGED: 'unchanged', ADDED: 'added', REMOVED: 'removed', MODIFIED: 'modified', TYPE_CHANGED: 'type_changed' }
1+
const TYPE = { UNCHANGED: 'unchanged', ADDED: 'added', REMOVED: 'removed', MODIFIED: 'modified', TYPE_CHANGED: 'type_changed' }
22

3-
const getType = v => v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v
3+
const typeOf = v => v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v
4+
const isObj = v => v !== null && typeof v === 'object'
5+
const keys = (a, b) => [...new Set([...Object.keys(a || {}), ...Object.keys(b || {})])]
46

5-
const isPrimitive = v => v === null || typeof v !== 'object'
7+
const node = (key, type, left, right, extra = {}) => ({ key, type, left, right, hasDiff: type !== TYPE.UNCHANGED, ...extra })
68

7-
const diffPrimitive = (left, right, key) => {
8-
if (left === right) return { key, type: TYPES.UNCHANGED, left, right, hasDiff: false }
9-
if (getType(left) !== getType(right)) return { key, type: TYPES.TYPE_CHANGED, left, right, hasDiff: true }
10-
return { key, type: TYPES.MODIFIED, left, right, hasDiff: true }
11-
}
9+
const container = (val, isArr) => isArr ? { isArray: true } : isObj(val) ? { isObject: true } : {}
1210

13-
const diffArray = (left, right, key) => {
14-
const len = Math.max(left?.length || 0, right?.length || 0)
15-
const children = Array.from({ length: len }, (_, i) => diff(left?.[i], right?.[i], i))
16-
const hasDiff = children.some(c => c.hasDiff)
17-
return { key, type: hasDiff ? TYPES.MODIFIED : TYPES.UNCHANGED, left, right, children, hasDiff, isArray: true }
18-
}
11+
const childMap = (val, side) => (v, k) => node(
12+
k, TYPE.UNCHANGED,
13+
side === 'added' ? undefined : v,
14+
side === 'added' ? v : undefined,
15+
isObj(v) && { children: mapChildren(v, side), ...container(v, Array.isArray(v)) }
16+
)
1917

20-
const diffObject = (left, right, key) => {
21-
const keys = [...new Set([...Object.keys(left || {}), ...Object.keys(right || {})])]
22-
const children = keys.map(k => diff(left?.[k], right?.[k], k))
23-
const hasDiff = children.some(c => c.hasDiff)
24-
return { key, type: hasDiff ? TYPES.MODIFIED : TYPES.UNCHANGED, left, right, children, hasDiff, isObject: true }
25-
}
18+
const mapChildren = (val, side) =>
19+
Array.isArray(val) ? val.map(childMap(val, side)) :
20+
isObj(val) ? Object.entries(val).map(([k, v]) => childMap(val, side)(v, k)) : []
2621

27-
const diff = (left, right, key = 'root') => {
28-
if (left === undefined && right !== undefined) return { key, type: TYPES.ADDED, left, right, hasDiff: true, ...(!isPrimitive(right) && { children: diffChildren(right), isArray: Array.isArray(right), isObject: !Array.isArray(right) && typeof right === 'object' }) }
29-
if (left !== undefined && right === undefined) return { key, type: TYPES.REMOVED, left, right, hasDiff: true, ...(!isPrimitive(left) && { children: diffChildren(left), isArray: Array.isArray(left), isObject: !Array.isArray(left) && typeof left === 'object' }) }
30-
if (isPrimitive(left) && isPrimitive(right)) return diffPrimitive(left, right, key)
31-
if (getType(left) !== getType(right)) return { key, type: TYPES.TYPE_CHANGED, left, right, hasDiff: true, children: [], isArray: Array.isArray(left) || Array.isArray(right), isObject: !Array.isArray(left) && typeof left === 'object' }
32-
if (Array.isArray(left)) return diffArray(left, right, key)
33-
return diffObject(left, right, key)
22+
const diffContainer = (left, right, key, isArr) => {
23+
const items = isArr
24+
? Array.from({ length: Math.max(left?.length || 0, right?.length || 0) }, (_, i) => diff(left?.[i], right?.[i], i))
25+
: keys(left, right).map(k => diff(left?.[k], right?.[k], k))
26+
const hasDiff = items.some(c => c.hasDiff)
27+
return node(key, hasDiff ? TYPE.MODIFIED : TYPE.UNCHANGED, left, right, { children: items, ...container(left, isArr) })
3428
}
3529

36-
const diffChildren = val => {
37-
if (Array.isArray(val)) return val.map((v, i) => ({ key: i, type: TYPES.UNCHANGED, left: v, right: v, hasDiff: false, ...(!isPrimitive(v) && { children: diffChildren(v), isArray: Array.isArray(v), isObject: !Array.isArray(v) && typeof v === 'object' }) }))
38-
if (typeof val === 'object' && val !== null) return Object.entries(val).map(([k, v]) => ({ key: k, type: TYPES.UNCHANGED, left: v, right: v, hasDiff: false, ...(!isPrimitive(v) && { children: diffChildren(v), isArray: Array.isArray(v), isObject: !Array.isArray(v) && typeof v === 'object' }) }))
39-
return []
30+
const diff = (left, right, key = 'root') => {
31+
if (left === undefined) return node(key, TYPE.ADDED, left, right, isObj(right) && { children: mapChildren(right, 'added'), ...container(right, Array.isArray(right)) })
32+
if (right === undefined) return node(key, TYPE.REMOVED, left, right, isObj(left) && { children: mapChildren(left, 'removed'), ...container(left, Array.isArray(left)) })
33+
if (!isObj(left) && !isObj(right)) return node(key, left === right ? TYPE.UNCHANGED : typeOf(left) !== typeOf(right) ? TYPE.TYPE_CHANGED : TYPE.MODIFIED, left, right)
34+
if (typeOf(left) !== typeOf(right)) return node(key, TYPE.TYPE_CHANGED, left, right, { children: [], ...container(left, Array.isArray(left)) })
35+
return diffContainer(left, right, key, Array.isArray(left))
4036
}
4137

42-
export { diff, TYPES }
38+
export { diff, TYPE }

0 commit comments

Comments
 (0)