json-diff-viewer-component 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -10
- package/package.json +1 -1
- package/src/lib/diff.js +2 -3
- package/src/lib/styles.js +14 -15
- package/src/lib/viewer.js +54 -14
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="public/logo.svg" alt="logo" height="128" />
|
|
2
|
+
<img src="https://raw.githubusercontent.com/metaory/json-diff-viewer-component/refs/heads/master/public/logo.svg" alt="logo" height="128" />
|
|
3
3
|
<h2>json-diff-viewer</h2>
|
|
4
4
|
<h5>
|
|
5
5
|
Compare JSON side-by-side, visually
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<br>
|
|
12
12
|
Perfect for debugging, API comparisons, and configuration diffs
|
|
13
13
|
</p>
|
|
14
|
-
<img src="public/screenshot.png" alt="demo" width="80%" />
|
|
14
|
+
<img src="https://raw.githubusercontent.com/metaory/json-diff-viewer-component/refs/heads/master/public/screenshot.png" alt="demo" width="80%" />
|
|
15
15
|
<h5>
|
|
16
16
|
<a href="https://metaory.github.io/json-diff-viewer-component/" target="_blank">metaory.github.io/json-diff-viewer-component</a>
|
|
17
17
|
</h5>
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
- Side-by-side synchronized scrolling
|
|
24
24
|
- Collapsible nodes (synced between panels)
|
|
25
25
|
- Diff indicators bubble up to parent nodes
|
|
26
|
-
- Stats summary (added/removed/modified
|
|
26
|
+
- Stats summary (added/removed/modified)
|
|
27
27
|
- Syntax highlighting
|
|
28
28
|
- Zero dependencies
|
|
29
29
|
- Shadow DOM encapsulation
|
|
@@ -123,12 +123,11 @@ watch(
|
|
|
123
123
|
|
|
124
124
|
## Diff Types
|
|
125
125
|
|
|
126
|
-
| Type
|
|
127
|
-
|
|
|
128
|
-
| Added
|
|
129
|
-
| Removed
|
|
130
|
-
| Modified
|
|
131
|
-
| Type Changed | Orange | Type mismatch (e.g. number → string) |
|
|
126
|
+
| Type | Color | Description |
|
|
127
|
+
| -------- | ------ | ------------------------ |
|
|
128
|
+
| Added | Green | Key exists only in right |
|
|
129
|
+
| Removed | Red | Key exists only in left |
|
|
130
|
+
| Modified | Yellow | Value changed |
|
|
132
131
|
|
|
133
132
|
## Styling
|
|
134
133
|
|
|
@@ -142,7 +141,6 @@ json-diff-viewer {
|
|
|
142
141
|
--add: #22c55e; /* Added items */
|
|
143
142
|
--rem: #ef4444; /* Removed items */
|
|
144
143
|
--mod: #eab308; /* Modified items */
|
|
145
|
-
--typ: #f97316; /* Type changed items */
|
|
146
144
|
|
|
147
145
|
/* Backgrounds */
|
|
148
146
|
--bg: #18181b; /* Main background */
|
package/package.json
CHANGED
package/src/lib/diff.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const TYPE = { UNCHANGED: 'unchanged', ADDED: 'added', REMOVED: 'removed', MODIFIED: 'modified'
|
|
1
|
+
const TYPE = { UNCHANGED: 'unchanged', ADDED: 'added', REMOVED: 'removed', MODIFIED: 'modified' }
|
|
2
2
|
|
|
3
3
|
const typeOf = (v) => {
|
|
4
4
|
if (v === null) return 'null';
|
|
@@ -57,11 +57,10 @@ const diff = (left, right, key = 'root') => {
|
|
|
57
57
|
}
|
|
58
58
|
if (!isObj(left) && !isObj(right)) {
|
|
59
59
|
if (left === right) return node(key, TYPE.UNCHANGED, left, right);
|
|
60
|
-
if (typeOf(left) !== typeOf(right)) return node(key, TYPE.TYPE_CHANGED, left, right);
|
|
61
60
|
return node(key, TYPE.MODIFIED, left, right);
|
|
62
61
|
}
|
|
63
62
|
if (typeOf(left) !== typeOf(right)) {
|
|
64
|
-
return node(key, TYPE.
|
|
63
|
+
return node(key, TYPE.MODIFIED, left, right, { children: [], ...container(left, Array.isArray(left)) });
|
|
65
64
|
}
|
|
66
65
|
return diffContainer(left, right, key, Array.isArray(left));
|
|
67
66
|
};
|
package/src/lib/styles.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export default `
|
|
2
2
|
:host {
|
|
3
|
-
--add: #22c55e; --rem: #ef4444; --mod: #eab308;
|
|
3
|
+
--add: #22c55e; --rem: #ef4444; --mod: #eab308;
|
|
4
4
|
--bg: #18181b; --bg2: #27272a; --bdr: #3f3f46;
|
|
5
5
|
--txt: #fafafa; --dim: #a1a1aa;
|
|
6
6
|
--key: #38bdf8; --str: #a78bfa; --num: #34d399; --bool: #fb923c; --nul: #f472b6; --br: #71717a;
|
|
@@ -15,7 +15,9 @@ export default `
|
|
|
15
15
|
.node { padding-left: 1.25rem; }
|
|
16
16
|
.node.root { padding-left: 0; }
|
|
17
17
|
.line { display: flex; align-items: flex-start; gap: 0.5rem; padding: 2px 4px; border-radius: 4px; cursor: pointer; transition: background .15s; }
|
|
18
|
-
.line:hover { background: rgba(
|
|
18
|
+
.line:hover { background: rgba(0,0,0,.03); }
|
|
19
|
+
.line.placeholder { cursor: default; pointer-events: none; }
|
|
20
|
+
.line.placeholder:hover { background: transparent; }
|
|
19
21
|
.tog { width: 1rem; flex-shrink: 0; color: var(--br); user-select: none; }
|
|
20
22
|
.tog:hover { color: var(--txt); }
|
|
21
23
|
.key { color: var(--key); }
|
|
@@ -26,27 +28,25 @@ export default `
|
|
|
26
28
|
.val-boolean { color: var(--bool); }
|
|
27
29
|
.val-null { color: var(--nul); font-style: italic; }
|
|
28
30
|
.br { color: var(--br); }
|
|
29
|
-
.diff-added { background: rgba(34,197,94,.15); }
|
|
30
|
-
.diff-removed { background: rgba(239,68,68,.15); }
|
|
31
|
-
.diff-modified { background: rgba(234,179,8,.15); }
|
|
32
|
-
.diff-
|
|
33
|
-
.diff-
|
|
34
|
-
.diff-
|
|
35
|
-
.diff-modified .key { color: var(--mod); }
|
|
36
|
-
.diff-type_changed .key { color: var(--typ); }
|
|
31
|
+
.node.diff-added { background: rgba(34,197,94,.15); }
|
|
32
|
+
.node.diff-removed { background: rgba(239,68,68,.15); }
|
|
33
|
+
.node.diff-modified { background: rgba(234,179,8,.15); }
|
|
34
|
+
.node.diff-added .key { color: var(--add); }
|
|
35
|
+
.node.diff-removed .key { color: var(--rem); }
|
|
36
|
+
.node.diff-modified .key { color: var(--mod); }
|
|
37
37
|
.dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; margin-top: 6px; }
|
|
38
38
|
.dot-added { background: var(--add); }
|
|
39
39
|
.dot-removed { background: var(--rem); }
|
|
40
40
|
.dot-modified { background: var(--mod); }
|
|
41
|
-
.dot-type_changed { background: var(--typ); }
|
|
42
41
|
.preview { color: var(--dim); font-style: italic; }
|
|
43
42
|
.preview::before { content: ' '; }
|
|
44
43
|
.preview::after { content: ' items'; }
|
|
45
|
-
.stats { display:
|
|
44
|
+
.stats { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 1rem; padding: .75rem 1rem; background: var(--bg); border-bottom: 2px solid var(--bdr); font-size: 12px; }
|
|
45
|
+
.stats-items { display: grid; grid-auto-flow: column; gap: 2rem; justify-content: start; }
|
|
46
46
|
.stats-buttons { display: flex; gap: 0.5rem; }
|
|
47
|
-
.btn-filter, .btn-collapse, .btn-expand { padding: 0.5rem; background: var(--bg2); border: 1px solid var(--bdr); border-radius:
|
|
47
|
+
.btn-filter, .btn-collapse, .btn-expand { padding: 0.5rem; background: var(--bg2); border: 1px solid var(--bdr); border-radius: 10px; color: var(--txt); cursor: pointer; transition: background .15s, border-color .15s; display: flex; align-items: center; justify-content: center; }
|
|
48
48
|
.btn-filter svg, .btn-collapse svg, .btn-expand svg { width: 18px; height: 18px; }
|
|
49
|
-
.btn-filter:hover, .btn-collapse:hover, .btn-expand:hover { background: rgba(
|
|
49
|
+
.btn-filter:hover, .btn-collapse:hover, .btn-expand:hover { background: rgba(0,0,0,.05); box-shadow: 0 0 4px var(--bdr); }
|
|
50
50
|
.btn-filter .checkbox-icon { opacity: 0.3; transition: opacity .15s; }
|
|
51
51
|
.btn-filter .checkbox-icon.checked { opacity: 1; }
|
|
52
52
|
.stat { display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: .35rem; }
|
|
@@ -54,6 +54,5 @@ export default `
|
|
|
54
54
|
.stat-added .dot { background: var(--add); }
|
|
55
55
|
.stat-removed .dot { background: var(--rem); }
|
|
56
56
|
.stat-modified .dot { background: var(--mod); }
|
|
57
|
-
.stat-type_changed .dot { background: var(--typ); }
|
|
58
57
|
.empty { padding: 2rem; color: var(--dim); }
|
|
59
58
|
`
|
package/src/lib/viewer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { diff, TYPE } from "./diff.js";
|
|
2
2
|
import styles from "./styles.js";
|
|
3
3
|
|
|
4
|
-
const STAT_TYPES = ["added", "removed", "modified"
|
|
4
|
+
const STAT_TYPES = ["added", "removed", "modified"];
|
|
5
5
|
|
|
6
6
|
const format = (val) => {
|
|
7
7
|
if (val === null) return ["null", "null"];
|
|
@@ -104,15 +104,18 @@ class JsonDiffViewer extends HTMLElement {
|
|
|
104
104
|
this.shadowRoot.innerHTML = `
|
|
105
105
|
<style>${styles}</style>
|
|
106
106
|
<div class="stats">
|
|
107
|
-
|
|
107
|
+
<div class="stats-items">
|
|
108
|
+
${STAT_TYPES.map((t) => `<div class="stat stat-${t}"><span class="dot"></span>${this.#stats[t]} ${t.replace("_", " ")}</div>`).join("")}
|
|
109
|
+
</div>
|
|
108
110
|
<div class="stats-buttons">
|
|
109
|
-
<button class="btn-filter" data-action="filter" aria-label="Show only changed">
|
|
110
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" class="checkbox-icon ${this.#showOnlyChanged ?
|
|
111
|
-
<path fill="currentColor" d="
|
|
111
|
+
<button class="btn-filter" data-action="filter" aria-label="Show only changed" title="Show only changed">
|
|
112
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" class="checkbox-icon ${this.#showOnlyChanged ? "checked" : ""}">
|
|
113
|
+
<path fill="currentColor" d="M4.25 12a7.75 7.75 0 1 1 15.5 0a7.75 7.75 0 0 1-15.5 0" opacity="0.5" />
|
|
114
|
+
<path fill="currentColor" d="M8.25 12a3.75 3.75 0 1 0 7.5 0a3.75 3.75 0 0 0-7.5 0" />
|
|
112
115
|
</svg>
|
|
113
116
|
</button>
|
|
114
|
-
<button class="btn-collapse" data-action="collapse"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M9 15H6q-.425 0-.712-.288T5 14t.288-.712T6 13h4q.425 0 .713.288T11 14v4q0 .425-.288.713T10 19t-.712-.288T9 18zm6-6h3q.425 0 .713.288T19 10t-.288.713T18 11h-4q-.425 0-.712-.288T13 10V6q0-.425.288-.712T14 5t.713.288T15 6z"/></svg></button>
|
|
115
|
-
<button class="btn-expand" data-action="expand"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M7 17h3q.425 0 .713.288T11 18t-.288.713T10 19H6q-.425 0-.712-.288T5 18v-4q0-.425.288-.712T6 13t.713.288T7 14zM17 7h-3q-.425 0-.712-.288T13 6t.288-.712T14 5h4q.425 0 .713.288T19 6v4q0 .425-.288.713T18 11t-.712-.288T17 10z"/></svg></button>
|
|
117
|
+
<button class="btn-collapse" data-action="collapse" title="Collapse all"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M9 15H6q-.425 0-.712-.288T5 14t.288-.712T6 13h4q.425 0 .713.288T11 14v4q0 .425-.288.713T10 19t-.712-.288T9 18zm6-6h3q.425 0 .713.288T19 10t-.288.713T18 11h-4q-.425 0-.712-.288T13 10V6q0-.425.288-.712T14 5t.713.288T15 6z"/></svg></button>
|
|
118
|
+
<button class="btn-expand" data-action="expand" title="Expand all"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M7 17h3q.425 0 .713.288T11 18t-.288.713T10 19H6q-.425 0-.712-.288T5 18v-4q0-.425.288-.712T6 13t.713.288T7 14zM17 7h-3q-.425 0-.712-.288T13 6t.288-.712T14 5h4q.425 0 .713.288T19 6v4q0 .425-.288.713T18 11t-.712-.288T17 10z"/></svg></button>
|
|
116
119
|
</div>
|
|
117
120
|
</div>
|
|
118
121
|
<div class="container">
|
|
@@ -126,6 +129,38 @@ class JsonDiffViewer extends HTMLElement {
|
|
|
126
129
|
this.#bind();
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
#placeholder(node, side, path, root = true) {
|
|
133
|
+
const currentPath = path ? `${path}.${node.key}` : String(node.key);
|
|
134
|
+
const value = node[side];
|
|
135
|
+
const keyHtml = root ? "" : `<span class="key" style="visibility: hidden;">${node.key}</span><span class="colon" style="visibility: hidden;">:</span>`;
|
|
136
|
+
const rootClass = root ? " root" : "";
|
|
137
|
+
|
|
138
|
+
if (!node.isArray && !node.isObject) {
|
|
139
|
+
if (value === undefined) {
|
|
140
|
+
return `<div class="node${rootClass}"><div class="line placeholder"><span class="tog"></span>${keyHtml}</div></div>`;
|
|
141
|
+
}
|
|
142
|
+
const [val, type] = format(value);
|
|
143
|
+
return `<div class="node${rootClass}"><div class="line placeholder"><span class="tog"></span>${keyHtml}<span class="val-${type}" style="visibility: hidden;">${val}</span></div></div>`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const [open, close] = node.isArray ? ["[", "]"] : ["{", "}"];
|
|
147
|
+
const children = node.children || [];
|
|
148
|
+
const isExpanded = this.#proxy[currentPath] !== false;
|
|
149
|
+
const filteredChildren = this.#showOnlyChanged
|
|
150
|
+
? children.filter((c) => c.hasDiff)
|
|
151
|
+
: children;
|
|
152
|
+
const childrenHtml = isExpanded
|
|
153
|
+
? filteredChildren.map((c) => this.#placeholder(c, side, currentPath, false)).join("")
|
|
154
|
+
: "";
|
|
155
|
+
const preview = `${filteredChildren.length}`;
|
|
156
|
+
|
|
157
|
+
if (!isExpanded) {
|
|
158
|
+
return `<div class="node${rootClass}"><div class="line placeholder"><span class="tog">▶</span>${keyHtml}<span class="br" style="visibility: hidden;">${open}</span><span class="preview" style="visibility: hidden;">${preview}</span><span class="br" style="visibility: hidden;">${close}</span></div></div>`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return `<div class="node${rootClass}"><div class="line placeholder"><span class="tog">▼</span>${keyHtml}<span class="br" style="visibility: hidden;">${open}</span></div>${childrenHtml}<div class="line placeholder"><span class="tog"></span><span class="br" style="visibility: hidden;">${close}</span></div></div>`;
|
|
162
|
+
}
|
|
163
|
+
|
|
129
164
|
#node(node, side, path, root = true) {
|
|
130
165
|
const currentPath = path ? `${path}.${node.key}` : String(node.key);
|
|
131
166
|
const value = node[side];
|
|
@@ -138,10 +173,18 @@ class JsonDiffViewer extends HTMLElement {
|
|
|
138
173
|
? ""
|
|
139
174
|
: `<span class="key">${node.key}</span><span class="colon">:</span>`;
|
|
140
175
|
const rootClass = root ? " root" : "";
|
|
176
|
+
const nodeDiffClass = hasDiff && !hasChildDiff ? ` ${diffClass}` : "";
|
|
177
|
+
|
|
178
|
+
if (value === undefined) {
|
|
179
|
+
if (node.children?.length) {
|
|
180
|
+
return this.#placeholder(node, side, path, root);
|
|
181
|
+
}
|
|
182
|
+
return `<div class="node${rootClass}"><div class="line placeholder"><span class="tog"></span>${root ? "" : `<span class="key" style="visibility: hidden;">${node.key}</span><span class="colon" style="visibility: hidden;">:</span>`}</div></div>`;
|
|
183
|
+
}
|
|
141
184
|
|
|
142
185
|
if (!node.isArray && !node.isObject) {
|
|
143
186
|
const [val, type] = format(value);
|
|
144
|
-
return `<div class="node${rootClass}"><div class="line
|
|
187
|
+
return `<div class="node${rootClass}${nodeDiffClass}"><div class="line"><span class="tog"></span>${dot}${keyHtml}<span class="val-${type}">${val}</span></div></div>`;
|
|
145
188
|
}
|
|
146
189
|
|
|
147
190
|
const [open, close] = node.isArray ? ["[", "]"] : ["{", "}"];
|
|
@@ -149,17 +192,14 @@ class JsonDiffViewer extends HTMLElement {
|
|
|
149
192
|
const filteredChildren = this.#showOnlyChanged
|
|
150
193
|
? node.children?.filter((c) => c.hasDiff) || []
|
|
151
194
|
: node.children || [];
|
|
152
|
-
const childrenHtml =
|
|
153
|
-
filteredChildren
|
|
154
|
-
.map((c) => this.#node(c, side, currentPath, false))
|
|
155
|
-
.join("") || "";
|
|
195
|
+
const childrenHtml = filteredChildren.map((c) => this.#node(c, side, currentPath, false)).join("");
|
|
156
196
|
const preview = `${filteredChildren.length}`;
|
|
157
197
|
|
|
158
198
|
if (!isExpanded) {
|
|
159
|
-
return `<div class="node${rootClass}"><div class="line
|
|
199
|
+
return `<div class="node${rootClass}${nodeDiffClass}"><div class="line" data-p="${currentPath}"><span class="tog">▶</span>${dot}${keyHtml}<span class="br">${open}</span><span class="preview">${preview}</span><span class="br">${close}</span></div></div>`;
|
|
160
200
|
}
|
|
161
201
|
|
|
162
|
-
return `<div class="node${rootClass}"><div class="line
|
|
202
|
+
return `<div class="node${rootClass}${nodeDiffClass}"><div class="line" data-p="${currentPath}"><span class="tog">▼</span>${dot}${keyHtml}<span class="br">${open}</span></div>${childrenHtml}<div class="line"><span class="tog"></span><span class="br">${close}</span></div></div>`;
|
|
163
203
|
}
|
|
164
204
|
|
|
165
205
|
#bind() {
|