@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 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
- --bg-body: #0f172a;
13
- --bg-header: #1e293b;
14
- --bg-input: #0f172a;
15
- --bg-button: #1e293b;
16
- --bg-button-hover: #334155;
17
- --bg-primary: #1d4ed8;
18
- --bg-primary-hover: #2563eb;
19
- --border: #334155;
20
- --border-button: #475569;
21
- --border-search-focus: #58a6ff;
22
- --text: #e2e8f0;
23
- --text-secondary: #94a3b8;
24
- --text-muted: #64748b;
25
- --text-label: #cbd5e1;
26
- --text-placeholder: #475569;
27
- --resizer: #334155;
28
- --resizer-active: #58a6ff;
29
- --tree-line: #334155;
30
- --arrow: #94a3b8;
31
- --key: #93c5fd;
32
- --colon: #94a3b8;
33
- --string: #86efac;
34
- --number: #fcd34d;
35
- --boolean: #f9a8d4;
36
- --null: #cbd5e1;
37
- --symbol: #c4b5fd;
38
- --meta: #64748b;
39
- --highlight-bg: #f0883e;
40
- --highlight-text: #0d1117;
41
- --highlight-current-bg: #58a6ff;
42
- --highlight-current-text: #fff;
43
- --error-text: #f87171;
44
- --error-bg: rgba(248,113,113,0.1);
45
- --error-border: rgba(248,113,113,0.2);
46
- --toggle-bg: #334155;
47
- --toggle-active: #58a6ff;
48
- --toggle-knob: #e2e8f0;
49
- --github-link: #94a3b8;
50
- --github-link-hover: #e2e8f0;
51
- --theme-btn-color: #94a3b8;
52
- --theme-btn-hover: #e2e8f0;
53
- --parsed-bg: rgba(251,191,36,0.06);
54
- --parsed-border: #f59e0b;
55
- --parsed-badge: #f59e0b;
56
- --parsed-badge-bg: rgba(245,158,11,0.15);
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: #f8fafc;
61
- --bg-header: #e2e8f0;
62
- --bg-input: #ffffff;
63
- --bg-button: #e2e8f0;
64
- --bg-button-hover: #cbd5e1;
65
- --bg-primary: #2563eb;
66
- --bg-primary-hover: #1d4ed8;
67
- --border: #cbd5e1;
68
- --border-button: #94a3b8;
69
- --border-search-focus: #2563eb;
70
- --text: #1e293b;
71
- --text-secondary: #475569;
72
- --text-muted: #64748b;
73
- --text-label: #334155;
74
- --text-placeholder: #94a3b8;
75
- --resizer: #cbd5e1;
76
- --resizer-active: #2563eb;
77
- --tree-line: #cbd5e1;
78
- --arrow: #64748b;
79
- --key: #1d4ed8;
80
- --colon: #64748b;
81
- --string: #16a34a;
82
- --number: #b45309;
83
- --boolean: #be185d;
84
- --null: #64748b;
85
- --symbol: #7c3aed;
86
- --meta: #94a3b8;
87
- --highlight-bg: #fbbf24;
88
- --highlight-text: #1e293b;
89
- --highlight-current-bg: #2563eb;
90
- --highlight-current-text: #fff;
91
- --error-text: #dc2626;
92
- --error-bg: rgba(220,38,38,0.08);
93
- --error-border: rgba(220,38,38,0.2);
94
- --toggle-bg: #cbd5e1;
95
- --toggle-active: #2563eb;
96
- --toggle-knob: #ffffff;
97
- --github-link: #64748b;
98
- --github-link-hover: #1e293b;
99
- --theme-btn-color: #64748b;
100
- --theme-btn-hover: #1e293b;
101
- --parsed-bg: rgba(245,158,11,0.07);
102
- --parsed-border: #d97706;
103
- --parsed-badge: #92400e;
104
- --parsed-badge-bg: rgba(217,119,6,0.12);
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: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
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: 12px;
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: 4px;
217
+ padding: 6px;
146
218
  border-radius: 6px;
147
- transition: color 0.2s;
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.2s;
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: 10px;
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: 5px;
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: inherit;
290
+ font-family: var(--font-mono);
213
291
  font-size: 13px;
214
- line-height: 1.6;
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-family: inherit;
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-hover);
251
- color: #fff;
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.2s;
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.2s;
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.5;
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: 8px;
440
+ gap: 6px;
354
441
  }
355
442
  .viewer .empty-state .icon {
356
- font-size: 36px;
357
- opacity: 0.5;
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 0 2px 8px;
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 rgba(88,166,255,0.4);
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: 24px;
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>{ } JSON Formatter</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="20" width="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
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="20" width="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none">
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="24" width="24" viewBox="0 0 16 16" fill="currentColor">
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-format" class="primary">✨ Format</button>
481
- <button id="btn-copy">📋 Copy</button>
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 and click <b>Format</b></div>
515
- <div style="font-size:12px">or press Ctrl+Enter</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 and click <b>Format</b></div><div style="font-size:12px">or press Ctrl+Enter</div></div>'
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 = 'Copied!'
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 = 'Link copied!'
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
- // Ctrl+Enter to format
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
- // ===== Paste auto-format =====
979
- jsonInput.addEventListener('paste', function () {
980
- setTimeout(function () {
981
- if (jsonInput.value.trim()) doFormat()
982
- }, 0)
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
- // Load saved preference, default to dark
1025
- var saved = localStorage.getItem('json-open-theme') || 'dark'
1026
- applyTheme(saved)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zjy4fun/json-open",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "JSON Formatter — A fast, zero-dependency JSON formatter & viewer (CLI + Web)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 }