Skip to content

Instantly share code, notes, and snippets.

@maksadbek
Created September 2, 2025 01:07
Show Gist options
  • Save maksadbek/811fe0670000e04bae13e73513cd8e19 to your computer and use it in GitHub Desktop.
Save maksadbek/811fe0670000e04bae13e73513cd8e19 to your computer and use it in GitHub Desktop.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>JSON Formatter • Split View</title>
<style>
:root { --left: 50%; --bg: #0f172a; --panel: #111827; --panel-2: #0b1220; --text: #e5e7eb; --muted: #94a3b8; --accent: #38bdf8; }
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; display: flex; flex-direction: column; height: 100vh; background: linear-gradient(180deg, var(--bg), var(--panel-2)); color: var(--text); font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
header { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: rgba(255,255,255,0.03); border-bottom: 1px solid rgba(148,163,184,0.15); }
header .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 12px var(--accent); }
header h1 { font-size: 14px; margin: 0; font-weight: 600; }
header .spacer { flex: 1; }
.panes { flex: 1; display: flex; min-height: 0; }
.pane { min-width: 180px; min-height: 0; display: flex; flex-direction: column; background: linear-gradient(180deg, var(--panel), var(--panel-2)); }
.pane.left { flex-basis: var(--left); }
.pane.right { flex: 1; border-left: 1px solid rgba(148,163,184,0.15); }
.titlebar { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: rgba(255,255,255,0.02); border-bottom: 1px solid rgba(148,163,184,0.1); color: var(--muted); }
.editor, .viewer { flex: 1; min-height: 0; position: relative; }
textarea { width: 100%; height: 100%; background: transparent; color: var(--text); border: none; outline: none; resize: none; padding: 16px; caret-color: var(--accent); tab-size: 2; }
#output { margin: 0; height: 100%; overflow: auto; padding: 16px; white-space: pre-wrap; }
.placeholder { color: var(--muted); }
.divider { width: 8px; cursor: col-resize; position: relative; background: rgba(148,163,184,0.12); border-left: 1px solid rgba(148,163,184,0.15); border-right: 1px solid rgba(148,163,184,0.15); }
body.resizing { cursor: col-resize; user-select: none; }
.actions { display: flex; gap: 8px; align-items: center; padding: 8px 12px; border-top: 1px solid rgba(148,163,184,0.15); background: rgba(255,255,255,0.02); }
.actions button, .actions select, .actions label { background: #0b1220; color: var(--text); border: 1px solid rgba(148,163,184,0.25); padding: 6px 10px; border-radius: 8px; font-weight: 600; }
.actions button { cursor: pointer; }
.node { margin: 2px 0; }
.header { user-select: none; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
.label { color: #93c5fd; }
.bracket { color: #94a3b8; }
.primitive { white-space: pre; }
.children { margin-left: 18px; border-left: 1px dashed rgba(148,163,184,0.2); padding-left: 10px; }
.meta { color: #94a3b8; margin-left: 4px; font-size: 12px; }
</style>
</head>
<body>
<header>
<span class="dot"></span>
<h1>JSON Formatter • Split View</h1>
<div class="spacer"></div>
</header>
<div class="panes" id="panes">
<section class="pane left">
<div class="titlebar">Input</div>
<div class="editor"><textarea id="input" placeholder='Paste JSON here…'></textarea></div>
<div class="actions">
<button id="formatBtn">Format</button>
<label><input type="checkbox" id="liveToggle" checked /> Live</label>
<select id="indentSel"><option value="2" selected>2 spaces</option><option value="4">4 spaces</option><option value="\t">Tabs</option></select>
</div>
</section>
<div id="divider" class="divider"></div>
<section class="pane right">
<div class="titlebar">Formatted</div>
<div class="viewer"><div id="output" class="placeholder">Formatted JSON will appear here…</div></div>
</section>
</div>
<footer>Tip: Paste JSON on the left. Drag divider. Press Ctrl/⌘+Enter.</footer>
<script>
const input = document.getElementById('input');
const output = document.getElementById('output');
const panes = document.getElementById('panes');
const divider = document.getElementById('divider');
const formatBtn = document.getElementById('formatBtn');
const liveToggle = document.getElementById('liveToggle');
const indentSel = document.getElementById('indentSel');
// Resizing
let dragging=false; function setLeft(x){const r=panes.getBoundingClientRect();let l=((x-r.left)/r.width)*100;l=Math.min(85,Math.max(15,l));panes.style.setProperty('--left',l+'%');}
divider.addEventListener('mousedown',e=>{dragging=true;document.body.classList.add('resizing');e.preventDefault();});
window.addEventListener('mousemove',e=>{if(dragging) setLeft(e.clientX)});
window.addEventListener('mouseup',()=>{dragging=false;document.body.classList.remove('resizing');});
// Utilities
const isObj = (v) => v !== null && typeof v === 'object';
const isArr = Array.isArray;
const jsonStr = (v) => JSON.stringify(v);
function renderNode(value, label = null) {
if (isObj(value)) {
const node = document.createElement('div'); node.className = 'node';
const header = document.createElement('div'); header.className = 'header';
const name = document.createElement('span'); name.className = 'label';
const open = document.createElement('span'); open.className = 'bracket';
const close = document.createElement('span'); close.className = 'bracket';
const children = document.createElement('div'); children.className = 'children';
const meta = document.createElement('span'); meta.className = 'meta';
const array = isArr(value);
const keys = array ? value.map((_, i) => String(i)) : Object.keys(value);
name.textContent = label !== null ? (array ? label + ': ' : label + ': ') : '';
open.textContent = array ? '[' : '{';
close.textContent = array ? ']' : '}';
meta.textContent = array ? ` (${value.length} items)` : ` (${keys.length} ${keys.length===1?'key':'keys'})`;
header.append(name, open, meta);
node.append(header, children, close);
keys.forEach((k, i) => {
const line = document.createElement('div'); line.className = 'node';
const childVal = array ? value[i] : value[k];
if (isObj(childVal)) {
line.append(renderNode(childVal, array ? k : '"'+k+'"'));
} else {
const leaf = document.createElement('div'); leaf.className = 'header';
const kspan = document.createElement('span'); kspan.className = 'label'; kspan.textContent = array ? k+': ' : '"'+k+'": ';
const vspan = document.createElement('span'); vspan.className = 'primitive'; vspan.textContent = jsonStr(childVal);
leaf.append(document.createTextNode(' '), kspan, vspan);
line.append(leaf);
}
children.append(line);
});
header.addEventListener('click', () => {
const hidden = children.style.display === 'none';
children.style.display = hidden ? '' : 'none';
});
return node;
}
const root = document.createElement('div'); root.className = 'node';
const line = document.createElement('div'); line.className = 'header';
if (label !== null) { const kspan = document.createElement('span'); kspan.className = 'label'; kspan.textContent = label+': '; line.append(kspan); }
const vspan = document.createElement('span'); vspan.className = 'primitive'; vspan.textContent = jsonStr(value);
line.append(vspan); root.append(line); return root;
}
function formatNow(){
const raw=input.value.trim();
if(!raw){output.textContent='Formatted JSON will appear here…';output.className='placeholder';return;}
try{ const data=JSON.parse(raw); output.innerHTML=''; output.appendChild(renderNode(data, null)); output.className=''; }
catch(err){
// On invalid JSON: show raw input with newlines rendered
output.className='';
output.textContent = input.value.replace(/\\n/g, '\n');
}
}
input.addEventListener('input',()=>{ if(liveToggle.checked) formatNow(); });
input.addEventListener('paste',()=>{ setTimeout(()=>{ if(liveToggle.checked) formatNow(); }, 0); });
formatBtn.addEventListener('click',formatNow);
window.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){ e.preventDefault(); formatNow(); } });
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment