@zjy4fun/json-open 0.3.3 → 0.3.5
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/index.html +280 -146
- package/package.json +1 -1
- package/tests/theme-preference.test.js +22 -0
- package/web/theme-preference.js +13 -0
package/index.html
CHANGED
|
@@ -5,111 +5,152 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>JSON Formatter</title>
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
8
11
|
<style>
|
|
9
|
-
/* ===== Theme Variables ===== */
|
|
12
|
+
/* ===== Theme Variables (OKLCH) ===== */
|
|
10
13
|
:root {
|
|
11
14
|
color-scheme: dark;
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
--bg-
|
|
17
|
-
--bg-
|
|
18
|
-
--bg-
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
--
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
--
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
--
|
|
34
|
-
--
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
--
|
|
38
|
-
--
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
--
|
|
42
|
-
--
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
--
|
|
46
|
-
--
|
|
47
|
-
--
|
|
48
|
-
--
|
|
49
|
-
--
|
|
50
|
-
--
|
|
51
|
-
--
|
|
52
|
-
--
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
--
|
|
56
|
-
--
|
|
15
|
+
--font-ui: 'Figtree', system-ui, sans-serif;
|
|
16
|
+
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
17
|
+
|
|
18
|
+
/* Surfaces — teal-tinted neutrals, hue 195 */
|
|
19
|
+
--bg-body: oklch(0.16 0.012 195);
|
|
20
|
+
--bg-header: oklch(0.21 0.012 195);
|
|
21
|
+
--bg-input: oklch(0.16 0.012 195);
|
|
22
|
+
--bg-button: oklch(0.24 0.012 195);
|
|
23
|
+
--bg-button-hover: oklch(0.30 0.012 195);
|
|
24
|
+
--bg-primary: oklch(0.55 0.14 195);
|
|
25
|
+
--bg-primary-hover: oklch(0.60 0.14 195);
|
|
26
|
+
|
|
27
|
+
/* Borders */
|
|
28
|
+
--border: oklch(0.30 0.010 195);
|
|
29
|
+
--border-button: oklch(0.38 0.010 195);
|
|
30
|
+
--border-search-focus: oklch(0.65 0.14 195);
|
|
31
|
+
|
|
32
|
+
/* Text */
|
|
33
|
+
--text: oklch(0.92 0.008 195);
|
|
34
|
+
--text-secondary: oklch(0.70 0.010 195);
|
|
35
|
+
--text-muted: oklch(0.55 0.010 195);
|
|
36
|
+
--text-label: oklch(0.82 0.008 195);
|
|
37
|
+
--text-placeholder: oklch(0.40 0.010 195);
|
|
38
|
+
|
|
39
|
+
/* Resizer */
|
|
40
|
+
--resizer: oklch(0.30 0.010 195);
|
|
41
|
+
--resizer-active: oklch(0.65 0.14 195);
|
|
42
|
+
|
|
43
|
+
/* Tree */
|
|
44
|
+
--tree-line: oklch(0.30 0.010 195);
|
|
45
|
+
--arrow: oklch(0.60 0.010 195);
|
|
46
|
+
|
|
47
|
+
/* Syntax — cohesive, slightly desaturated */
|
|
48
|
+
--key: oklch(0.78 0.09 195);
|
|
49
|
+
--colon: oklch(0.60 0.010 195);
|
|
50
|
+
--string: oklch(0.76 0.11 155);
|
|
51
|
+
--number: oklch(0.78 0.10 80);
|
|
52
|
+
--boolean: oklch(0.74 0.09 340);
|
|
53
|
+
--null: oklch(0.62 0.02 195);
|
|
54
|
+
--symbol: oklch(0.74 0.09 285);
|
|
55
|
+
--meta: oklch(0.55 0.010 195);
|
|
56
|
+
|
|
57
|
+
/* Search highlights */
|
|
58
|
+
--highlight-bg: oklch(0.75 0.14 70);
|
|
59
|
+
--highlight-text: oklch(0.18 0.01 195);
|
|
60
|
+
--highlight-current-bg: oklch(0.60 0.14 195);
|
|
61
|
+
--highlight-current-text: oklch(0.98 0.005 195);
|
|
62
|
+
|
|
63
|
+
/* Errors */
|
|
64
|
+
--error-text: oklch(0.70 0.16 25);
|
|
65
|
+
--error-bg: oklch(0.22 0.03 25);
|
|
66
|
+
--error-border: oklch(0.30 0.05 25);
|
|
67
|
+
|
|
68
|
+
/* Toggle */
|
|
69
|
+
--toggle-bg: oklch(0.30 0.012 195);
|
|
70
|
+
--toggle-active: oklch(0.60 0.14 195);
|
|
71
|
+
--toggle-knob: oklch(0.92 0.008 195);
|
|
72
|
+
|
|
73
|
+
/* Links / icon buttons */
|
|
74
|
+
--github-link: oklch(0.60 0.010 195);
|
|
75
|
+
--github-link-hover: oklch(0.92 0.008 195);
|
|
76
|
+
--theme-btn-color: oklch(0.60 0.010 195);
|
|
77
|
+
--theme-btn-hover: oklch(0.92 0.008 195);
|
|
78
|
+
|
|
79
|
+
/* Parsed JSON indicator — background tint only, no border-left */
|
|
80
|
+
--parsed-bg: oklch(0.22 0.03 80);
|
|
81
|
+
--parsed-badge: oklch(0.75 0.10 80);
|
|
82
|
+
--parsed-badge-bg: oklch(0.26 0.04 80);
|
|
57
83
|
}
|
|
84
|
+
|
|
58
85
|
:root.light {
|
|
59
86
|
color-scheme: light;
|
|
60
|
-
--bg-body:
|
|
61
|
-
--bg-header:
|
|
62
|
-
--bg-input:
|
|
63
|
-
--bg-button:
|
|
64
|
-
--bg-button-hover:
|
|
65
|
-
--bg-primary:
|
|
66
|
-
--bg-primary-hover:
|
|
67
|
-
|
|
68
|
-
--border
|
|
69
|
-
--border-
|
|
70
|
-
--
|
|
71
|
-
|
|
72
|
-
--text
|
|
73
|
-
--text-
|
|
74
|
-
--text-
|
|
75
|
-
--
|
|
76
|
-
--
|
|
77
|
-
|
|
78
|
-
--
|
|
79
|
-
--
|
|
80
|
-
|
|
81
|
-
--
|
|
82
|
-
--
|
|
83
|
-
|
|
84
|
-
--
|
|
85
|
-
--
|
|
86
|
-
--
|
|
87
|
-
--
|
|
88
|
-
--
|
|
89
|
-
--
|
|
90
|
-
--
|
|
91
|
-
--
|
|
92
|
-
|
|
93
|
-
--
|
|
94
|
-
--
|
|
95
|
-
--
|
|
96
|
-
--
|
|
97
|
-
|
|
98
|
-
--
|
|
99
|
-
--
|
|
100
|
-
--
|
|
101
|
-
|
|
102
|
-
--
|
|
103
|
-
--
|
|
104
|
-
--
|
|
105
|
-
|
|
87
|
+
--bg-body: oklch(0.97 0.005 195);
|
|
88
|
+
--bg-header: oklch(0.93 0.008 195);
|
|
89
|
+
--bg-input: oklch(0.99 0.003 195);
|
|
90
|
+
--bg-button: oklch(0.93 0.008 195);
|
|
91
|
+
--bg-button-hover: oklch(0.88 0.010 195);
|
|
92
|
+
--bg-primary: oklch(0.50 0.14 195);
|
|
93
|
+
--bg-primary-hover: oklch(0.45 0.14 195);
|
|
94
|
+
|
|
95
|
+
--border: oklch(0.85 0.008 195);
|
|
96
|
+
--border-button: oklch(0.72 0.008 195);
|
|
97
|
+
--border-search-focus: oklch(0.50 0.14 195);
|
|
98
|
+
|
|
99
|
+
--text: oklch(0.22 0.012 195);
|
|
100
|
+
--text-secondary: oklch(0.42 0.010 195);
|
|
101
|
+
--text-muted: oklch(0.55 0.008 195);
|
|
102
|
+
--text-label: oklch(0.30 0.010 195);
|
|
103
|
+
--text-placeholder: oklch(0.65 0.008 195);
|
|
104
|
+
|
|
105
|
+
--resizer: oklch(0.85 0.008 195);
|
|
106
|
+
--resizer-active: oklch(0.50 0.14 195);
|
|
107
|
+
|
|
108
|
+
--tree-line: oklch(0.82 0.008 195);
|
|
109
|
+
--arrow: oklch(0.50 0.010 195);
|
|
110
|
+
|
|
111
|
+
--key: oklch(0.42 0.12 195);
|
|
112
|
+
--colon: oklch(0.50 0.008 195);
|
|
113
|
+
--string: oklch(0.42 0.12 155);
|
|
114
|
+
--number: oklch(0.48 0.10 80);
|
|
115
|
+
--boolean: oklch(0.45 0.10 340);
|
|
116
|
+
--null: oklch(0.55 0.02 195);
|
|
117
|
+
--symbol: oklch(0.45 0.10 285);
|
|
118
|
+
--meta: oklch(0.62 0.008 195);
|
|
119
|
+
|
|
120
|
+
--highlight-bg: oklch(0.85 0.12 80);
|
|
121
|
+
--highlight-text: oklch(0.20 0.01 195);
|
|
122
|
+
--highlight-current-bg: oklch(0.50 0.14 195);
|
|
123
|
+
--highlight-current-text: oklch(0.98 0.005 195);
|
|
124
|
+
|
|
125
|
+
--error-text: oklch(0.48 0.16 25);
|
|
126
|
+
--error-bg: oklch(0.95 0.03 25);
|
|
127
|
+
--error-border: oklch(0.85 0.05 25);
|
|
128
|
+
|
|
129
|
+
--toggle-bg: oklch(0.85 0.008 195);
|
|
130
|
+
--toggle-active: oklch(0.50 0.14 195);
|
|
131
|
+
--toggle-knob: oklch(0.99 0.003 195);
|
|
132
|
+
|
|
133
|
+
--github-link: oklch(0.50 0.010 195);
|
|
134
|
+
--github-link-hover: oklch(0.22 0.012 195);
|
|
135
|
+
--theme-btn-color: oklch(0.50 0.010 195);
|
|
136
|
+
--theme-btn-hover: oklch(0.22 0.012 195);
|
|
137
|
+
|
|
138
|
+
--parsed-bg: oklch(0.95 0.03 80);
|
|
139
|
+
--parsed-badge: oklch(0.45 0.10 80);
|
|
140
|
+
--parsed-badge-bg: oklch(0.90 0.04 80);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ===== Base ===== */
|
|
106
144
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
145
|
+
|
|
107
146
|
body {
|
|
108
|
-
font-family:
|
|
147
|
+
font-family: var(--font-mono);
|
|
109
148
|
background: var(--bg-body);
|
|
110
149
|
color: var(--text);
|
|
111
150
|
height: 100vh;
|
|
112
151
|
overflow: hidden;
|
|
152
|
+
-webkit-font-smoothing: antialiased;
|
|
153
|
+
-moz-osx-font-smoothing: grayscale;
|
|
113
154
|
}
|
|
114
155
|
|
|
115
156
|
/* ===== Header ===== */
|
|
@@ -124,14 +165,45 @@
|
|
|
124
165
|
flex-shrink: 0;
|
|
125
166
|
}
|
|
126
167
|
.header h1 {
|
|
168
|
+
font-family: var(--font-ui);
|
|
127
169
|
font-size: 15px;
|
|
128
170
|
font-weight: 600;
|
|
129
171
|
color: var(--text);
|
|
130
172
|
white-space: nowrap;
|
|
173
|
+
letter-spacing: -0.01em;
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 6px;
|
|
177
|
+
}
|
|
178
|
+
.header h1 span {
|
|
179
|
+
color: var(--text-muted);
|
|
180
|
+
font-weight: 400;
|
|
181
|
+
}
|
|
182
|
+
.home-btn {
|
|
183
|
+
appearance: none;
|
|
184
|
+
background: none;
|
|
185
|
+
border: none;
|
|
186
|
+
color: inherit;
|
|
187
|
+
font: inherit;
|
|
188
|
+
cursor: pointer;
|
|
189
|
+
padding: 2px 4px;
|
|
190
|
+
border-radius: 6px;
|
|
191
|
+
display: inline-flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
gap: 6px;
|
|
194
|
+
transition: background 0.15s ease-out, color 0.15s ease-out;
|
|
195
|
+
line-height: 1;
|
|
196
|
+
}
|
|
197
|
+
.home-btn:hover {
|
|
198
|
+
background: var(--bg-button-hover);
|
|
199
|
+
}
|
|
200
|
+
.home-btn:focus-visible {
|
|
201
|
+
outline: 2px solid var(--border-search-focus);
|
|
202
|
+
outline-offset: 1px;
|
|
131
203
|
}
|
|
132
204
|
.header .actions {
|
|
133
205
|
display: flex;
|
|
134
|
-
gap:
|
|
206
|
+
gap: 8px;
|
|
135
207
|
margin-left: auto;
|
|
136
208
|
align-items: center;
|
|
137
209
|
}
|
|
@@ -142,18 +214,20 @@
|
|
|
142
214
|
cursor: pointer;
|
|
143
215
|
display: flex;
|
|
144
216
|
align-items: center;
|
|
145
|
-
padding:
|
|
217
|
+
padding: 6px;
|
|
146
218
|
border-radius: 6px;
|
|
147
|
-
transition: color 0.
|
|
219
|
+
transition: color 0.15s ease-out;
|
|
148
220
|
}
|
|
149
221
|
.theme-toggle:hover {
|
|
150
222
|
color: var(--theme-btn-hover);
|
|
151
223
|
}
|
|
152
224
|
.github-link {
|
|
153
225
|
color: var(--github-link);
|
|
154
|
-
transition: color 0.
|
|
226
|
+
transition: color 0.15s ease-out;
|
|
155
227
|
display: flex;
|
|
156
228
|
align-items: center;
|
|
229
|
+
padding: 4px;
|
|
230
|
+
border-radius: 6px;
|
|
157
231
|
}
|
|
158
232
|
.github-link:hover {
|
|
159
233
|
color: var(--github-link-hover);
|
|
@@ -180,23 +254,27 @@
|
|
|
180
254
|
display: flex;
|
|
181
255
|
align-items: center;
|
|
182
256
|
padding: 0 16px;
|
|
183
|
-
gap:
|
|
257
|
+
gap: 8px;
|
|
184
258
|
flex-shrink: 0;
|
|
259
|
+
font-family: var(--font-ui);
|
|
185
260
|
font-size: 13px;
|
|
186
261
|
color: var(--text-secondary);
|
|
187
262
|
}
|
|
188
263
|
.panel-header .label {
|
|
189
264
|
font-weight: 600;
|
|
190
265
|
color: var(--text-label);
|
|
266
|
+
font-size: 12px;
|
|
267
|
+
text-transform: uppercase;
|
|
268
|
+
letter-spacing: 0.04em;
|
|
191
269
|
}
|
|
192
270
|
|
|
193
271
|
/* ===== Resizer ===== */
|
|
194
272
|
.resizer {
|
|
195
|
-
width:
|
|
273
|
+
width: 4px;
|
|
196
274
|
background: var(--resizer);
|
|
197
275
|
cursor: col-resize;
|
|
198
276
|
flex-shrink: 0;
|
|
199
|
-
transition: background 0.15s;
|
|
277
|
+
transition: background 0.15s ease-out;
|
|
200
278
|
}
|
|
201
279
|
.resizer:hover, .resizer.active {
|
|
202
280
|
background: var(--resizer-active);
|
|
@@ -209,9 +287,9 @@
|
|
|
209
287
|
border: none;
|
|
210
288
|
background: var(--bg-input);
|
|
211
289
|
color: var(--text);
|
|
212
|
-
font-family:
|
|
290
|
+
font-family: var(--font-mono);
|
|
213
291
|
font-size: 13px;
|
|
214
|
-
line-height: 1.
|
|
292
|
+
line-height: 1.65;
|
|
215
293
|
padding: 16px;
|
|
216
294
|
outline: none;
|
|
217
295
|
tab-size: 2;
|
|
@@ -232,6 +310,7 @@
|
|
|
232
310
|
flex-shrink: 0;
|
|
233
311
|
}
|
|
234
312
|
button {
|
|
313
|
+
font-family: var(--font-ui);
|
|
235
314
|
border: 1px solid var(--border-button);
|
|
236
315
|
background: var(--bg-button);
|
|
237
316
|
color: var(--text);
|
|
@@ -239,16 +318,20 @@
|
|
|
239
318
|
padding: 5px 10px;
|
|
240
319
|
cursor: pointer;
|
|
241
320
|
font-size: 12px;
|
|
242
|
-
font-
|
|
321
|
+
font-weight: 500;
|
|
243
322
|
white-space: nowrap;
|
|
323
|
+
transition: background 0.12s ease-out, border-color 0.12s ease-out;
|
|
244
324
|
}
|
|
245
325
|
button:hover {
|
|
246
326
|
background: var(--bg-button-hover);
|
|
247
327
|
}
|
|
328
|
+
button:active {
|
|
329
|
+
transform: scale(0.98);
|
|
330
|
+
}
|
|
248
331
|
button.primary {
|
|
249
332
|
background: var(--bg-primary);
|
|
250
|
-
border-color: var(--bg-primary
|
|
251
|
-
color:
|
|
333
|
+
border-color: var(--bg-primary);
|
|
334
|
+
color: oklch(0.98 0.005 195);
|
|
252
335
|
}
|
|
253
336
|
button.primary:hover {
|
|
254
337
|
background: var(--bg-primary-hover);
|
|
@@ -262,24 +345,26 @@
|
|
|
262
345
|
margin-left: auto;
|
|
263
346
|
}
|
|
264
347
|
.search-wrap input {
|
|
348
|
+
font-family: var(--font-mono);
|
|
265
349
|
border: 1px solid var(--border-button);
|
|
266
350
|
background: var(--bg-input);
|
|
267
351
|
color: var(--text);
|
|
268
352
|
border-radius: 6px;
|
|
269
353
|
padding: 5px 10px;
|
|
270
354
|
font-size: 12px;
|
|
271
|
-
font-family: inherit;
|
|
272
355
|
width: 180px;
|
|
273
356
|
outline: none;
|
|
274
|
-
transition: border-color 0.
|
|
357
|
+
transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out;
|
|
275
358
|
}
|
|
276
359
|
.search-wrap input:focus {
|
|
277
360
|
border-color: var(--border-search-focus);
|
|
361
|
+
box-shadow: 0 0 0 2px oklch(0.60 0.14 195 / 0.15);
|
|
278
362
|
}
|
|
279
363
|
.search-wrap input::placeholder {
|
|
280
364
|
color: var(--text-muted);
|
|
281
365
|
}
|
|
282
366
|
.search-count {
|
|
367
|
+
font-family: var(--font-ui);
|
|
283
368
|
font-size: 11px;
|
|
284
369
|
color: var(--text-muted);
|
|
285
370
|
min-width: 50px;
|
|
@@ -294,6 +379,7 @@
|
|
|
294
379
|
display: flex;
|
|
295
380
|
align-items: center;
|
|
296
381
|
gap: 6px;
|
|
382
|
+
font-family: var(--font-ui);
|
|
297
383
|
font-size: 12px;
|
|
298
384
|
color: var(--text-secondary);
|
|
299
385
|
}
|
|
@@ -314,7 +400,7 @@
|
|
|
314
400
|
inset: 0;
|
|
315
401
|
background: var(--toggle-bg);
|
|
316
402
|
border-radius: 20px;
|
|
317
|
-
transition: background 0.
|
|
403
|
+
transition: background 0.15s ease-out;
|
|
318
404
|
}
|
|
319
405
|
.toggle .slider::before {
|
|
320
406
|
content: '';
|
|
@@ -325,7 +411,7 @@
|
|
|
325
411
|
bottom: 3px;
|
|
326
412
|
background: var(--toggle-knob);
|
|
327
413
|
border-radius: 50%;
|
|
328
|
-
transition: transform 0.2s;
|
|
414
|
+
transition: transform 0.2s cubic-bezier(0.33, 1, 0.68, 1);
|
|
329
415
|
}
|
|
330
416
|
.toggle input:checked + .slider {
|
|
331
417
|
background: var(--toggle-active);
|
|
@@ -339,7 +425,7 @@
|
|
|
339
425
|
flex: 1;
|
|
340
426
|
overflow: auto;
|
|
341
427
|
padding: 16px;
|
|
342
|
-
line-height: 1.
|
|
428
|
+
line-height: 1.55;
|
|
343
429
|
font-size: 13px;
|
|
344
430
|
}
|
|
345
431
|
.viewer .empty-state {
|
|
@@ -349,12 +435,19 @@
|
|
|
349
435
|
justify-content: center;
|
|
350
436
|
height: 100%;
|
|
351
437
|
color: var(--text-placeholder);
|
|
438
|
+
font-family: var(--font-ui);
|
|
352
439
|
font-size: 14px;
|
|
353
|
-
gap:
|
|
440
|
+
gap: 6px;
|
|
354
441
|
}
|
|
355
442
|
.viewer .empty-state .icon {
|
|
356
|
-
font-
|
|
357
|
-
|
|
443
|
+
font-family: var(--font-mono);
|
|
444
|
+
font-size: 32px;
|
|
445
|
+
opacity: 0.4;
|
|
446
|
+
font-weight: 500;
|
|
447
|
+
}
|
|
448
|
+
.viewer .empty-state .hint {
|
|
449
|
+
font-size: 12px;
|
|
450
|
+
color: var(--text-muted);
|
|
358
451
|
}
|
|
359
452
|
.viewer .error-msg {
|
|
360
453
|
color: var(--error-text);
|
|
@@ -378,6 +471,8 @@
|
|
|
378
471
|
content: '\25B8';
|
|
379
472
|
margin-right: 6px;
|
|
380
473
|
color: var(--arrow);
|
|
474
|
+
display: inline-block;
|
|
475
|
+
transition: transform 0.12s ease-out;
|
|
381
476
|
}
|
|
382
477
|
details[open] > summary::before {
|
|
383
478
|
content: '\25BE';
|
|
@@ -397,17 +492,18 @@
|
|
|
397
492
|
.symbol { color: var(--symbol); }
|
|
398
493
|
.meta { color: var(--meta); }
|
|
399
494
|
|
|
400
|
-
/* ===== Parsed JSON string highlight ===== */
|
|
495
|
+
/* ===== Parsed JSON string highlight — background tint, no border-left ===== */
|
|
401
496
|
.parsed-json {
|
|
402
497
|
background: var(--parsed-bg);
|
|
403
|
-
border-left: 2px solid var(--parsed-border);
|
|
404
498
|
border-radius: 4px;
|
|
405
|
-
padding: 2px
|
|
499
|
+
padding: 2px 6px 2px 8px;
|
|
406
500
|
margin: 2px 0;
|
|
407
501
|
}
|
|
408
502
|
.parsed-json > summary::after {
|
|
409
503
|
content: 'parsed';
|
|
504
|
+
font-family: var(--font-ui);
|
|
410
505
|
font-size: 10px;
|
|
506
|
+
font-weight: 500;
|
|
411
507
|
color: var(--parsed-badge);
|
|
412
508
|
background: var(--parsed-badge-bg);
|
|
413
509
|
border-radius: 3px;
|
|
@@ -426,33 +522,67 @@
|
|
|
426
522
|
mark.highlight.current {
|
|
427
523
|
background: var(--highlight-current-bg);
|
|
428
524
|
color: var(--highlight-current-text);
|
|
429
|
-
box-shadow: 0 0 0 2px
|
|
525
|
+
box-shadow: 0 0 0 2px oklch(0.60 0.14 195 / 0.35);
|
|
430
526
|
}
|
|
431
527
|
|
|
432
528
|
/* ===== Status Bar ===== */
|
|
433
529
|
.status-bar {
|
|
434
|
-
height:
|
|
530
|
+
height: 26px;
|
|
435
531
|
background: var(--bg-header);
|
|
436
532
|
border-top: 1px solid var(--border);
|
|
437
533
|
display: flex;
|
|
438
534
|
align-items: center;
|
|
439
535
|
padding: 0 16px;
|
|
536
|
+
font-family: var(--font-ui);
|
|
440
537
|
font-size: 11px;
|
|
441
538
|
color: var(--text-muted);
|
|
442
539
|
gap: 16px;
|
|
443
540
|
flex-shrink: 0;
|
|
444
541
|
}
|
|
542
|
+
|
|
543
|
+
/* ===== Scrollbar ===== */
|
|
544
|
+
.viewer::-webkit-scrollbar,
|
|
545
|
+
.input-area::-webkit-scrollbar {
|
|
546
|
+
width: 8px;
|
|
547
|
+
height: 8px;
|
|
548
|
+
}
|
|
549
|
+
.viewer::-webkit-scrollbar-track,
|
|
550
|
+
.input-area::-webkit-scrollbar-track {
|
|
551
|
+
background: transparent;
|
|
552
|
+
}
|
|
553
|
+
.viewer::-webkit-scrollbar-thumb,
|
|
554
|
+
.input-area::-webkit-scrollbar-thumb {
|
|
555
|
+
background: var(--border);
|
|
556
|
+
border-radius: 4px;
|
|
557
|
+
}
|
|
558
|
+
.viewer::-webkit-scrollbar-thumb:hover,
|
|
559
|
+
.input-area::-webkit-scrollbar-thumb:hover {
|
|
560
|
+
background: var(--border-button);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* ===== Reduced Motion ===== */
|
|
564
|
+
@media (prefers-reduced-motion: reduce) {
|
|
565
|
+
*, *::before, *::after {
|
|
566
|
+
transition-duration: 0.01ms !important;
|
|
567
|
+
animation-duration: 0.01ms !important;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
445
570
|
</style>
|
|
446
571
|
</head>
|
|
447
572
|
<body>
|
|
448
573
|
<div class="header">
|
|
449
|
-
<h1>
|
|
574
|
+
<h1>
|
|
575
|
+
<button type="button" class="home-btn" id="home-btn" aria-label="Back to home and clear shared JSON">
|
|
576
|
+
<span>{ }</span>
|
|
577
|
+
JSON Formatter
|
|
578
|
+
</button>
|
|
579
|
+
</h1>
|
|
450
580
|
<div class="actions">
|
|
451
581
|
<button class="theme-toggle" id="theme-toggle" title="Toggle light/dark mode">
|
|
452
|
-
<svg id="icon-moon" height="
|
|
582
|
+
<svg id="icon-moon" height="18" width="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
453
583
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
454
584
|
</svg>
|
|
455
|
-
<svg id="icon-sun" height="
|
|
585
|
+
<svg id="icon-sun" height="18" width="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none">
|
|
456
586
|
<circle cx="12" cy="12" r="5"/>
|
|
457
587
|
<line x1="12" y1="1" x2="12" y2="3"/>
|
|
458
588
|
<line x1="12" y1="21" x2="12" y2="23"/>
|
|
@@ -465,7 +595,7 @@
|
|
|
465
595
|
</svg>
|
|
466
596
|
</button>
|
|
467
597
|
<a href="https://github.com/zjy4fun/json-open" target="_blank" rel="noopener noreferrer" class="github-link" title="View on GitHub">
|
|
468
|
-
<svg height="
|
|
598
|
+
<svg height="22" width="22" viewBox="0 0 16 16" fill="currentColor">
|
|
469
599
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
|
470
600
|
</svg>
|
|
471
601
|
</a>
|
|
@@ -477,9 +607,8 @@
|
|
|
477
607
|
<div class="panel panel-left" id="panel-left">
|
|
478
608
|
<div class="panel-header">
|
|
479
609
|
<span class="label">Input</span>
|
|
480
|
-
<button id="btn-
|
|
481
|
-
<button id="btn-
|
|
482
|
-
<button id="btn-share">🔗 Share</button>
|
|
610
|
+
<button id="btn-copy">Copy</button>
|
|
611
|
+
<button id="btn-share">Share</button>
|
|
483
612
|
</div>
|
|
484
613
|
<textarea class="input-area" id="json-input" placeholder="Paste or type JSON here..." spellcheck="false"></textarea>
|
|
485
614
|
</div>
|
|
@@ -511,8 +640,8 @@
|
|
|
511
640
|
<div class="viewer" id="viewer">
|
|
512
641
|
<div class="empty-state">
|
|
513
642
|
<div class="icon">{ }</div>
|
|
514
|
-
<div>Paste JSON on the left
|
|
515
|
-
<div
|
|
643
|
+
<div>Paste or type JSON on the left</div>
|
|
644
|
+
<div class="hint">It will format automatically</div>
|
|
516
645
|
</div>
|
|
517
646
|
</div>
|
|
518
647
|
</div>
|
|
@@ -523,7 +652,10 @@
|
|
|
523
652
|
<span id="status-size" style="margin-left:auto"></span>
|
|
524
653
|
</div>
|
|
525
654
|
|
|
655
|
+
<script src="./web/theme-preference.js"></script>
|
|
526
656
|
<script>
|
|
657
|
+
var resolveInitialTheme = window.JsonOpenThemePreference.resolveInitialTheme
|
|
658
|
+
|
|
527
659
|
// ===== Utility =====
|
|
528
660
|
function escapeHtml(str) {
|
|
529
661
|
return str
|
|
@@ -660,7 +792,6 @@
|
|
|
660
792
|
// ===== Elements =====
|
|
661
793
|
var jsonInput = document.getElementById('json-input')
|
|
662
794
|
var viewer = document.getElementById('viewer')
|
|
663
|
-
var btnFormat = document.getElementById('btn-format')
|
|
664
795
|
var btnCopy = document.getElementById('btn-copy')
|
|
665
796
|
var btnShare = document.getElementById('btn-share')
|
|
666
797
|
var btnExpandAll = document.getElementById('expand-all')
|
|
@@ -675,6 +806,7 @@
|
|
|
675
806
|
var statusSize = document.getElementById('status-size')
|
|
676
807
|
var panelLeft = document.getElementById('panel-left')
|
|
677
808
|
var resizer = document.getElementById('resizer')
|
|
809
|
+
var homeBtn = document.getElementById('home-btn')
|
|
678
810
|
|
|
679
811
|
var currentParsed = null
|
|
680
812
|
var currentDeepParsed = null
|
|
@@ -683,7 +815,7 @@
|
|
|
683
815
|
function doFormat() {
|
|
684
816
|
var input = jsonInput.value.trim()
|
|
685
817
|
if (!input) {
|
|
686
|
-
viewer.innerHTML = '<div class="empty-state"><div class="icon">{ }</div><div>Paste JSON on the left
|
|
818
|
+
viewer.innerHTML = '<div class="empty-state"><div class="icon">{ }</div><div>Paste or type JSON on the left</div><div class="hint">It will format automatically</div></div>'
|
|
687
819
|
statusInfo.textContent = 'Ready'
|
|
688
820
|
statusSize.textContent = ''
|
|
689
821
|
toggleWrap.classList.add('hidden')
|
|
@@ -729,14 +861,12 @@
|
|
|
729
861
|
}
|
|
730
862
|
|
|
731
863
|
// ===== Buttons =====
|
|
732
|
-
btnFormat.addEventListener('click', doFormat)
|
|
733
|
-
|
|
734
864
|
btnCopy.addEventListener('click', function () {
|
|
735
865
|
var text = jsonInput.value.trim()
|
|
736
866
|
if (!text) return
|
|
737
867
|
navigator.clipboard.writeText(text).then(function () {
|
|
738
868
|
var orig = btnCopy.textContent
|
|
739
|
-
btnCopy.textContent = '
|
|
869
|
+
btnCopy.textContent = 'Copied!'
|
|
740
870
|
setTimeout(function () { btnCopy.textContent = orig }, 1500)
|
|
741
871
|
})
|
|
742
872
|
})
|
|
@@ -762,18 +892,19 @@
|
|
|
762
892
|
var url = location.origin + location.pathname + '#json=' + encoded
|
|
763
893
|
navigator.clipboard.writeText(url).then(function () {
|
|
764
894
|
var orig = btnShare.textContent
|
|
765
|
-
btnShare.textContent = '
|
|
895
|
+
btnShare.textContent = 'Link copied!'
|
|
766
896
|
statusInfo.textContent = 'Share link copied (' + formatBytes(url.length) + ')'
|
|
767
897
|
setTimeout(function () { btnShare.textContent = orig }, 2000)
|
|
768
898
|
})
|
|
769
899
|
})
|
|
770
900
|
|
|
771
|
-
//
|
|
901
|
+
// ===== Home =====
|
|
902
|
+
homeBtn.addEventListener('click', function () {
|
|
903
|
+
window.location.assign(location.origin + location.pathname + location.search)
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
// Input behavior
|
|
772
907
|
jsonInput.addEventListener('keydown', function (e) {
|
|
773
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
774
|
-
e.preventDefault()
|
|
775
|
-
doFormat()
|
|
776
|
-
}
|
|
777
908
|
// Tab support
|
|
778
909
|
if (e.key === 'Tab') {
|
|
779
910
|
e.preventDefault()
|
|
@@ -975,11 +1106,13 @@
|
|
|
975
1106
|
}
|
|
976
1107
|
})
|
|
977
1108
|
|
|
978
|
-
// =====
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1109
|
+
// ===== Auto format on input =====
|
|
1110
|
+
var inputDebounceTimer
|
|
1111
|
+
jsonInput.addEventListener('input', function () {
|
|
1112
|
+
clearTimeout(inputDebounceTimer)
|
|
1113
|
+
inputDebounceTimer = setTimeout(function () {
|
|
1114
|
+
doFormat()
|
|
1115
|
+
}, 200)
|
|
983
1116
|
})
|
|
984
1117
|
|
|
985
1118
|
// ===== Load from URL hash =====
|
|
@@ -1021,9 +1154,10 @@
|
|
|
1021
1154
|
}
|
|
1022
1155
|
}
|
|
1023
1156
|
|
|
1024
|
-
|
|
1025
|
-
var
|
|
1026
|
-
|
|
1157
|
+
var saved = localStorage.getItem('json-open-theme')
|
|
1158
|
+
var systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
1159
|
+
var initialTheme = resolveInitialTheme({ savedTheme: saved, systemPrefersDark: systemPrefersDark })
|
|
1160
|
+
applyTheme(initialTheme)
|
|
1027
1161
|
|
|
1028
1162
|
themeBtn.addEventListener('click', function () {
|
|
1029
1163
|
var isLight = root.classList.contains('light')
|
package/package.json
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { resolveInitialTheme } from '../web/theme-preference.js'
|
|
5
|
+
|
|
6
|
+
test('uses saved theme when localStorage has a valid value', () => {
|
|
7
|
+
assert.equal(resolveInitialTheme({ savedTheme: 'light', systemPrefersDark: true }), 'light')
|
|
8
|
+
assert.equal(resolveInitialTheme({ savedTheme: 'dark', systemPrefersDark: false }), 'dark')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('falls back to browser dark preference when no saved theme exists', () => {
|
|
12
|
+
assert.equal(resolveInitialTheme({ savedTheme: null, systemPrefersDark: true }), 'dark')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('falls back to browser light preference when no saved theme exists', () => {
|
|
16
|
+
assert.equal(resolveInitialTheme({ savedTheme: null, systemPrefersDark: false }), 'light')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('ignores invalid saved values and uses browser preference instead', () => {
|
|
20
|
+
assert.equal(resolveInitialTheme({ savedTheme: 'system', systemPrefersDark: true }), 'dark')
|
|
21
|
+
assert.equal(resolveInitialTheme({ savedTheme: 'auto', systemPrefersDark: false }), 'light')
|
|
22
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function resolveInitialTheme({ savedTheme, systemPrefersDark }) {
|
|
2
|
+
if (savedTheme === 'light' || savedTheme === 'dark') {
|
|
3
|
+
return savedTheme
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return systemPrefersDark ? 'dark' : 'light'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (typeof window !== 'undefined') {
|
|
10
|
+
window.JsonOpenThemePreference = { resolveInitialTheme }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { resolveInitialTheme }
|