json-diff-viewer-component 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <div align="center">
2
- <img src="https://raw.githubusercontent.com/metaory/json-diff-viewer-component/master/public/logo.svg" alt="logo" height="128" />
2
+ <img src="https://raw.githubusercontent.com/metaory/json-diff-viewer-component/master/public/logo.png" alt="logo" height="128" />
3
3
  <h2>json-diff-viewer</h2>
4
4
  <h5>
5
5
  Compare JSON side-by-side, visually
@@ -36,24 +36,50 @@ npm i json-diff-viewer-component
36
36
 
37
37
  ## Usage
38
38
 
39
- Import the package to register the `<json-diff-viewer>` custom element, then call `setData()` with both JSON values:
39
+ Every integration needs three things:
40
+
41
+ 1. Import the package once to register the custom element.
42
+ 2. Add `<json-diff-viewer>` to the page.
43
+ 3. Provide both values using one of the data APIs below.
44
+
45
+ Import the package:
40
46
 
41
47
  ```js
42
48
  import "json-diff-viewer-component";
49
+ ```
50
+
51
+ ```html
52
+ <json-diff-viewer></json-diff-viewer>
53
+ ```
43
54
 
55
+ Choose one data API. Root values must be non-null.
56
+
57
+ ### `setData(left, right)` (Recommended)
58
+
59
+ Use for dynamic data. Both values update in one render:
60
+
61
+ ```js
44
62
  const viewer = document.querySelector("json-diff-viewer");
45
- viewer.setData(leftObj, rightObj);
63
+ viewer.setData(
64
+ { name: "foo", enabled: true },
65
+ { name: "bar", enabled: true },
66
+ );
46
67
  ```
47
68
 
48
- For testing or extension, the class is also exported:
69
+ ### Properties (Alternative)
70
+
71
+ Use when assigning JavaScript values independently. Rendering starts once both sides are set:
49
72
 
50
73
  ```js
51
- import { JsonDiffViewer } from "json-diff-viewer-component";
74
+ viewer.left = { name: "foo" };
75
+ viewer.right = { name: "bar" };
76
+
77
+ console.log(viewer.left, viewer.right);
52
78
  ```
53
79
 
54
- ### HTML Attributes
80
+ ### HTML Attributes (Alternative)
55
81
 
56
- Static JSON can be passed as attributes. Values must be valid JSON strings:
82
+ Use only for small, static, inline JSON. Each value is parsed with `JSON.parse()`, so invalid JSON throws:
57
83
 
58
84
  ```html
59
85
  <json-diff-viewer
@@ -62,33 +88,32 @@ Static JSON can be passed as attributes. Values must be valid JSON strings:
62
88
  ></json-diff-viewer>
63
89
  ```
64
90
 
65
- - `JSON.parse` parses attribute values. Invalid JSON throws at parse time.
66
- - Prefer `setData()` or property setters for dynamic or object data
91
+ Prefer `setData()` or properties for objects already available in JavaScript.
67
92
 
68
- ### Properties
93
+ ### Class Export
69
94
 
70
- Set `left` and `right` as JavaScript values. Set both before the viewer draws a diff:
95
+ The registered class is exported only when a class reference is needed for testing or extension:
71
96
 
72
97
  ```js
73
- viewer.left = { name: "foo" };
74
- viewer.right = { name: "bar" };
75
-
76
- viewer.left; // read current left value
77
- viewer.right; // read current right value
98
+ import { JsonDiffViewer } from "json-diff-viewer-component";
78
99
  ```
79
100
 
80
101
  ### Built-in Controls
81
102
 
82
- The component toolbar includes:
103
+ No setup is required. The component toolbar includes:
83
104
 
84
105
  - **Show only changed**: filter toggle (default: **on**); hides unchanged nodes
85
106
  - **Collapse all** / **Expand all**: bulk expand/collapse
86
107
  - **Node toggles**: click any object/array line to expand/collapse (synced across both panels)
87
108
 
109
+ ### Framework Usage
110
+
111
+ The component remains the same custom element and uses `setData()` for reactive values.
112
+
88
113
  <details>
89
- <summary>Framework Examples</summary>
114
+ <summary>React and Vue examples</summary>
90
115
 
91
- ### React
116
+ #### React
92
117
 
93
118
  ```jsx
94
119
  import { useEffect, useRef } from "react";
@@ -107,7 +132,7 @@ function DiffViewer({ left, right }) {
107
132
  }
108
133
  ```
109
134
 
110
- ### Vue
135
+ #### Vue
111
136
 
112
137
  ```vue
113
138
  <template>
@@ -115,25 +140,15 @@ function DiffViewer({ left, right }) {
115
140
  </template>
116
141
 
117
142
  <script setup>
118
- import { ref, watch } from "vue";
143
+ import { ref, watchEffect } from "vue";
119
144
  import "json-diff-viewer-component";
120
145
 
121
- const props = defineProps({
122
- left: Object,
123
- right: Object,
124
- });
125
-
146
+ const props = defineProps(["left", "right"]);
126
147
  const viewerRef = ref(null);
127
148
 
128
- watch(
129
- () => [props.left, props.right],
130
- () => {
131
- if (viewerRef.value) {
132
- viewerRef.value.setData(props.left, props.right);
133
- }
134
- },
135
- { immediate: true },
136
- );
149
+ watchEffect(() => {
150
+ viewerRef.value?.setData(props.left, props.right);
151
+ });
137
152
  </script>
138
153
  ```
139
154
 
@@ -141,18 +156,20 @@ watch(
141
156
 
142
157
  ## Diff Types
143
158
 
144
- | Type | Color | Description |
145
- | -------- | ------ | ------------------------ |
146
- | Added | Green | Key exists only in right |
147
- | Removed | Red | Key exists only in left |
148
- | Modified | Yellow | Value changed |
159
+ | Type | Default | Description |
160
+ | --- | --- | --- |
161
+ | Added | Green | Key exists only on the right |
162
+ | Removed | Red | Key exists only on the left |
163
+ | Modified | Yellow | Values differ, or a container has changed descendants |
149
164
 
150
165
  ## Styling
151
166
 
152
- Override CSS custom properties (design tokens) on `json-diff-viewer`. Tokens live on `:host`; you set them from outside the shadow DOM.
167
+ The component is fully usable without style configuration. Override CSS custom properties on `json-diff-viewer`. They inherit through the host into its shadow DOM. State foreground tokens also drive their derived backgrounds unless those backgrounds are overridden separately.
153
168
 
154
169
  ### Design Tokens
155
170
 
171
+ Tokens inherit through the host. Override only the values your theme needs.
172
+
156
173
  ```css
157
174
  json-diff-viewer {
158
175
  /* Diff colors */
@@ -171,8 +188,18 @@ json-diff-viewer {
171
188
  --txt: #fafafa; /* Primary text */
172
189
  --dim: #a1a1aa; /* Dimmed/secondary text */
173
190
 
174
- /* Controls */
175
- --slider: var(--bdr); /* Slider toggle active color */
191
+ /* State backgrounds */
192
+ --add-bg: color-mix(in srgb, var(--add) 15%, transparent);
193
+ --rem-bg: color-mix(in srgb, var(--rem) 15%, transparent);
194
+ --mod-bg: color-mix(in srgb, var(--mod) 15%, transparent);
195
+
196
+ /* Controls and interaction */
197
+ --hover: rgb(0 0 0 / 3%); /* Diff row hover */
198
+ --control-bg: var(--bg2); /* Action button background */
199
+ --control-hover: rgb(0 0 0 / 5%); /* Action button hover */
200
+ --control-bdr: var(--bdr); /* Action button border */
201
+ --slider: var(--bdr); /* Slider active track */
202
+ --slider-thumb: var(--br); /* Slider thumb */
176
203
 
177
204
  /* Syntax highlighting */
178
205
  --key: #38bdf8; /* Object keys */
@@ -206,6 +233,43 @@ json-diff-viewer {
206
233
  }
207
234
  ```
208
235
 
236
+ ### Shadow Parts
237
+
238
+ Use `::part()` for structural overrides that are not shared design tokens:
239
+
240
+ | Parts | Elements |
241
+ | --- | --- |
242
+ | `toolbar`, `legend`, `legend-item`, `legend-added`, `legend-removed`, `legend-modified` | Summary and state legend |
243
+ | `actions`, `filter`, `filter-track`, `action-button`, `collapse-button`, `expand-button` | Viewer controls |
244
+ | `content`, `panel`, `panel-left`, `panel-right` | Diff layout and panels |
245
+ | `node`, `node-added`, `node-removed`, `node-modified`, `line` | Diff nodes and rows |
246
+ | `toggle`, `marker`, `marker-added`, `marker-removed`, `marker-modified` | Node controls and state markers |
247
+ | `key`, `separator`, `value`, `value-string`, `value-number`, `value-boolean`, `value-null` | JSON content |
248
+ | `bracket`, `preview`, `empty` | Supporting content and empty state |
249
+
250
+ Elements can expose multiple parts, so generic and specific selectors compose:
251
+
252
+ ```css
253
+ json-diff-viewer {
254
+ --add: lime;
255
+ --add-bg: color-mix(in srgb, lime 20%, transparent);
256
+ }
257
+
258
+ json-diff-viewer::part(toolbar) {
259
+ border-block-end-width: 4px;
260
+ }
261
+
262
+ json-diff-viewer::part(node-added) {
263
+ border-inline-start: 3px solid var(--add);
264
+ }
265
+
266
+ json-diff-viewer::part(collapse-button) {
267
+ border-radius: 999px;
268
+ }
269
+ ```
270
+
271
+ Parts are the supported structural API. Internal classes and other shadow elements remain private. `::part()` cannot select descendants, so target each listed part directly. Changing layout, padding, or typography on `node` and `line` can desynchronize corresponding rows between panels.
272
+
209
273
  ### Sizing
210
274
 
211
275
  Set a height to get scrolling. Without one, the viewer grows to fit all content. Default border-radius is `12px`.
@@ -237,6 +301,7 @@ json-diff-viewer {
237
301
  ```bash
238
302
  npm run dev # start dev server
239
303
  npm run build # build for production
304
+ npm run preview # preview the production build
240
305
  ```
241
306
 
242
307
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-diff-viewer-component",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "type": "module",
5
5
  "description": "Vanilla JS web component for side-by-side JSON diff visualization",
6
6
  "bugs": "https://github.com/metaory/json-diff-viewer-component/issues",
package/src/lib/styles.js CHANGED
@@ -1,10 +1,15 @@
1
1
  export default `
2
2
  :host {
3
3
  --add: #22c55e; --rem: #ef4444; --mod: #eab308;
4
+ --add-bg: color-mix(in srgb, var(--add) 15%, transparent);
5
+ --rem-bg: color-mix(in srgb, var(--rem) 15%, transparent);
6
+ --mod-bg: color-mix(in srgb, var(--mod) 15%, transparent);
4
7
  --bg: #18181b; --bg2: #27272a; --bdr: #3f3f46;
5
8
  --txt: #fafafa; --dim: #a1a1aa;
6
9
  --key: #38bdf8; --str: #a78bfa; --num: #34d399; --bool: #fb923c; --nul: #f472b6; --br: #71717a;
7
- --slider: var(--bdr);
10
+ --hover: rgb(0 0 0 / 3%);
11
+ --control-bg: var(--bg2); --control-hover: rgb(0 0 0 / 5%); --control-bdr: var(--bdr);
12
+ --slider: var(--bdr); --slider-thumb: var(--br);
8
13
  display: flex; flex-direction: column; font: 13px 'JetBrains Mono', 'Fira Code', monospace;
9
14
  background: var(--bg); color: var(--txt); border-radius: 12px; overflow: hidden;
10
15
  }
@@ -15,7 +20,7 @@ export default `
15
20
  .node { padding-left: 1.25rem; }
16
21
  .node.root { padding-left: 0; }
17
22
  .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(0,0,0,.03); }
23
+ .line:hover { background: var(--hover); }
19
24
  .line.placeholder { cursor: default; pointer-events: none; }
20
25
  .line.placeholder:hover { background: transparent; }
21
26
  .tog { width: 1rem; flex-shrink: 0; color: var(--br); user-select: none; }
@@ -28,9 +33,9 @@ export default `
28
33
  .val-boolean { color: var(--bool); }
29
34
  .val-null { color: var(--nul); font-style: italic; }
30
35
  .br { color: var(--br); }
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); }
36
+ .node.diff-added { background: var(--add-bg); }
37
+ .node.diff-removed { background: var(--rem-bg); }
38
+ .node.diff-modified { background: var(--mod-bg); }
34
39
  .node.diff-added .key { color: var(--add); }
35
40
  .node.diff-removed .key { color: var(--rem); }
36
41
  .node.diff-modified .key { color: var(--mod); }
@@ -47,13 +52,13 @@ export default `
47
52
  .switch { display: inline-block; cursor: pointer; }
48
53
  .checkbox { display: none; }
49
54
  .slider { width: 48px; height: 24px; background-color: var(--bdr); border-radius: 16px; overflow: hidden; display: flex; align-items: center; border: 3px solid transparent; transition: .3s; box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.25) inset; cursor: pointer; }
50
- .slider::before { content: ''; display: block; width: 100%; height: 100%; background-color: var(--br); transform: translateX(-24px); border-radius: 16px; transition: .3s; box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.25); }
55
+ .slider::before { content: ''; display: block; width: 100%; height: 100%; background-color: var(--slider-thumb); transform: translateX(-24px); border-radius: 16px; transition: .3s; box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.25); }
51
56
  .checkbox:checked ~ .slider::before { transform: translateX(24px); box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.25); }
52
57
  .checkbox:checked ~ .slider { background-color: var(--slider); }
53
58
  .checkbox:active ~ .slider::before { transform: translate(0); }
54
- .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; }
59
+ .btn-collapse, .btn-expand { padding: 0.5rem; background: var(--control-bg); border: 1px solid var(--control-bdr); border-radius: 10px; color: var(--txt); cursor: pointer; transition: background .15s, border-color .15s; display: flex; align-items: center; justify-content: center; }
55
60
  .btn-collapse svg, .btn-expand svg { width: 18px; height: 18px; }
56
- .btn-collapse:hover, .btn-expand:hover { background: rgba(0,0,0,.05); box-shadow: 0 0 4px var(--bdr); }
61
+ .btn-collapse:hover, .btn-expand:hover { background: var(--control-hover); box-shadow: 0 0 4px var(--control-bdr); }
57
62
  .stat { display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: .35rem; }
58
63
  .stat .dot { width: 8px; height: 8px; }
59
64
  .stat-added .dot { background: var(--add); }
package/src/lib/viewer.js CHANGED
@@ -2,6 +2,8 @@ import { diff, TYPE } from "./diff.js";
2
2
  import styles from "./styles.js";
3
3
 
4
4
  const STAT_TYPES = ["added", "removed", "modified"];
5
+ const ENTITIES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
6
+ const escapeHtml = (value) => String(value).replace(/[&<>"']/g, (char) => ENTITIES[char]);
5
7
 
6
8
  const format = (val) => {
7
9
  if (val === null) return ["null", "null"];
@@ -20,7 +22,7 @@ const filterChildren = (children, showOnlyChanged) =>
20
22
  const isExpanded = (proxy, path) => proxy[path] !== false;
21
23
 
22
24
  const buildKeyHtml = (key, root, hidden = false) =>
23
- root ? "" : `<span class="key"${hidden ? ' style="visibility: hidden;"' : ""}>${key}</span><span class="colon"${hidden ? ' style="visibility: hidden;"' : ""}>:</span>`;
25
+ root ? "" : `<span class="key" part="key"${hidden ? ' style="visibility: hidden;"' : ""}>${escapeHtml(key)}</span><span class="colon" part="separator"${hidden ? ' style="visibility: hidden;"' : ""}>:</span>`;
24
26
 
25
27
  const buildRootClass = (root) => root ? " root" : "";
26
28
 
@@ -136,30 +138,30 @@ class JsonDiffViewer extends HTMLElement {
136
138
 
137
139
  #render() {
138
140
  if (!this.#tree) {
139
- this.shadowRoot.innerHTML = `<style>${styles}</style><div class="empty">Provide left and right JSON</div>`;
141
+ this.shadowRoot.innerHTML = `<style>${styles}</style><div class="empty" part="empty">Provide left and right JSON</div>`;
140
142
  return;
141
143
  }
142
144
  const panel = this.shadowRoot.querySelector('.panel');
143
145
  const scroll = { top: panel?.scrollTop || 0, left: panel?.scrollLeft || 0 };
144
146
  this.shadowRoot.innerHTML = `
145
147
  <style>${styles}</style>
146
- <div class="stats">
147
- <div class="stats-items">
148
- ${STAT_TYPES.map((t) => `<div class="stat stat-${t}"><span class="dot"></span>${this.#stats[t]} ${t.replace("_", " ")}</div>`).join("")}
148
+ <div class="stats" part="toolbar">
149
+ <div class="stats-items" part="legend">
150
+ ${STAT_TYPES.map((t) => `<div class="stat stat-${t}" part="legend-item legend-${t}"><span class="dot" part="marker marker-${t}"></span>${this.#stats[t]} ${t.replace("_", " ")}</div>`).join("")}
149
151
  </div>
150
- <div class="stats-buttons">
151
- <label class="switch" aria-label="Show only changed" title="Show only changed">
152
+ <div class="stats-buttons" part="actions">
153
+ <label class="switch" part="filter" aria-label="Show only changed" title="Show only changed">
152
154
  <input type="checkbox" class="checkbox" data-action="filter" ${this.#showOnlyChanged ? "checked" : ""}>
153
- <div class="slider"></div>
155
+ <div class="slider" part="filter-track"></div>
154
156
  </label>
155
- <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>
156
- <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>
157
+ <button class="btn-collapse" part="action-button collapse-button" 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>
158
+ <button class="btn-expand" part="action-button expand-button" 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>
157
159
  </div>
158
160
  </div>
159
- <div class="container">
161
+ <div class="container" part="content">
160
162
  ${["left", "right"]
161
163
  .map((side) => {
162
- return `<div class="panel" data-side="${side}">${this.#renderNode(this.#tree, side, "")}</div>`;
164
+ return `<div class="panel" part="panel panel-${side}" data-side="${side}">${this.#renderNode(this.#tree, side, "")}</div>`;
163
165
  })
164
166
  .join("")}
165
167
  </div>`;
@@ -182,18 +184,18 @@ class JsonDiffViewer extends HTMLElement {
182
184
  const otherValue = side === 'left' ? node.right : node.left;
183
185
  const [val, type] = format(otherValue);
184
186
  const hiddenKey = buildKeyHtml(node.key, root, true);
185
- return `<div class="node${rootClass}"><div class="line placeholder">${hiddenKey}<span class="val-${type}" style="visibility: hidden;">${val}</span></div></div>`;
187
+ return `<div class="node${rootClass}" part="node"><div class="line placeholder" part="line">${hiddenKey}<span class="val-${type}" part="value value-${type}" style="visibility: hidden;">${escapeHtml(val)}</span></div></div>`;
186
188
  }
187
189
 
188
190
  if (value !== undefined && !node.isArray && !node.isObject) {
189
191
  const [val, type] = format(value);
190
192
  if (placeholder) {
191
- return `<div class="node${rootClass}"><div class="line placeholder">${keyHtml}<span class="val-${type}"${hidden}>${val}</span></div></div>`;
193
+ return `<div class="node${rootClass}" part="node"><div class="line placeholder" part="line">${keyHtml}<span class="val-${type}" part="value value-${type}"${hidden}>${escapeHtml(val)}</span></div></div>`;
192
194
  }
193
195
  const hasDiff = node.hasDiff && node.type !== TYPE.UNCHANGED;
194
196
  const diffClass = hasDiff ? `diff-${node.type}` : "";
195
197
  const nodeDiffClass = hasDiff ? ` ${diffClass}` : "";
196
- return `<div class="node${rootClass}${nodeDiffClass}"><div class="line"><span class="tog"></span>${keyHtml}<span class="val-${type}">${val}</span></div></div>`;
198
+ return `<div class="node${rootClass}${nodeDiffClass}" part="node${hasDiff ? ` node-${node.type}` : ""}"><div class="line" part="line"><span class="tog" part="toggle"></span>${keyHtml}<span class="val-${type}" part="value value-${type}">${escapeHtml(val)}</span></div></div>`;
197
199
  }
198
200
 
199
201
  const [open, close] = getBrackets(node.isArray);
@@ -207,25 +209,25 @@ class JsonDiffViewer extends HTMLElement {
207
209
 
208
210
  if (placeholder) {
209
211
  if (!expanded) {
210
- return `<div class="node${rootClass}"><div class="line placeholder">${keyHtml}<span class="br"${hidden}>${open}</span><span class="preview"${hidden}>${preview}</span><span class="br"${hidden}>${close}</span></div></div>`;
212
+ return `<div class="node${rootClass}" part="node"><div class="line placeholder" part="line">${keyHtml}<span class="br" part="bracket"${hidden}>${open}</span><span class="preview" part="preview"${hidden}>${preview}</span><span class="br" part="bracket"${hidden}>${close}</span></div></div>`;
211
213
  }
212
- return `<div class="node${rootClass}"><div class="line placeholder">${keyHtml}<span class="br"${hidden}>${open}</span></div>${childrenHtml}<div class="line placeholder"><span class="br"${hidden}>${close}</span></div></div>`;
214
+ return `<div class="node${rootClass}" part="node"><div class="line placeholder" part="line">${keyHtml}<span class="br" part="bracket"${hidden}>${open}</span></div>${childrenHtml}<div class="line placeholder" part="line"><span class="br" part="bracket"${hidden}>${close}</span></div></div>`;
213
215
  }
214
216
 
215
217
  const hasDiff = node.hasDiff && node.type !== TYPE.UNCHANGED;
216
218
  const diffClass = hasDiff ? `diff-${node.type}` : "";
217
219
  const hasChildDiff = node.hasDiff && children.some((c) => c.hasDiff);
218
220
  const dotType = node.type === TYPE.UNCHANGED ? "modified" : node.type;
219
- const dot = hasChildDiff ? `<span class="dot dot-${dotType}"></span>` : "";
221
+ const dot = hasChildDiff ? `<span class="dot dot-${dotType}" part="marker marker-${dotType}"></span>` : "";
220
222
  const nodeDiffClass = hasDiff && !hasChildDiff ? ` ${diffClass}` : "";
221
223
  const toggle = expanded ? "▼" : "▶";
222
- const dataPath = ` data-p="${currentPath}"`;
224
+ const dataPath = ` data-p="${escapeHtml(currentPath)}"`;
223
225
 
224
226
  if (!expanded) {
225
- return `<div class="node${rootClass}${nodeDiffClass}"><div class="line"${dataPath}><span class="tog">${toggle}</span>${dot}${keyHtml}<span class="br">${open}</span><span class="preview">${preview}</span><span class="br">${close}</span></div></div>`;
227
+ return `<div class="node${rootClass}${nodeDiffClass}" part="node${hasDiff ? ` node-${node.type}` : ""}"><div class="line" part="line"${dataPath}><span class="tog" part="toggle">${toggle}</span>${dot}${keyHtml}<span class="br" part="bracket">${open}</span><span class="preview" part="preview">${preview}</span><span class="br" part="bracket">${close}</span></div></div>`;
226
228
  }
227
229
 
228
- return `<div class="node${rootClass}${nodeDiffClass}"><div class="line"${dataPath}><span class="tog">${toggle}</span>${dot}${keyHtml}<span class="br">${open}</span></div>${childrenHtml}<div class="line"><span class="tog"></span><span class="br">${close}</span></div></div>`;
230
+ return `<div class="node${rootClass}${nodeDiffClass}" part="node${hasDiff ? ` node-${node.type}` : ""}"><div class="line" part="line"${dataPath}><span class="tog" part="toggle">${toggle}</span>${dot}${keyHtml}<span class="br" part="bracket">${open}</span></div>${childrenHtml}<div class="line" part="line"><span class="tog" part="toggle"></span><span class="br" part="bracket">${close}</span></div></div>`;
229
231
  }
230
232
 
231
233
  #bind() {