Lightweight console JS/CSS, inspect, snippets, commands
// ==UserScript==
// @name Vanilla
// @namespace http://tampermonkey.net/
// @version 15.1
// @description Lightweight console JS/CSS, inspect, snippets, commands
// @author placeholdernamexd
// @match *://*/*
// @grant GM_addStyle
// @run-at document-end
// @license MIT
// @icon https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExd2ppNmY0YWg3OTBnaXMyaDl0bnNwZnFsMjl0dXZ3NTNtbWxqYzh4bCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/fHyqSLg41HNYF68PRi/giphy.gif
// ==/UserScript==
(function(){
const isMac = /Mac/i.test(navigator.platform);
let settings = {
theme: localStorage.t || 'd',
opacity: +(localStorage.o) || 95,
fontSize: +(localStorage.f) || 13,
dock: localStorage.d || 'br',
pin: localStorage.p === 't',
mode: localStorage.m || 'j',
snippets: JSON.parse(localStorage.sn || '{}'),
history: JSON.parse(localStorage.h || '[]'),
filter: 'all',
keyMod: localStorage.km || (isMac ? 'Meta' : 'Alt'),
keyCode: localStorage.kc || 'KeyX'
};
let histIndex = -1;
let panel, input, logs, resizeHandle, pinBtn, modeBtn, styleTag;
// helpers
function save() {
localStorage.t = settings.theme;
localStorage.o = settings.opacity;
localStorage.f = settings.fontSize;
localStorage.d = settings.dock;
localStorage.p = settings.pin ? 't' : 'f';
localStorage.m = settings.mode;
localStorage.sn = JSON.stringify(settings.snippets);
localStorage.h = JSON.stringify(settings.history);
localStorage.km = settings.keyMod;
localStorage.kc = settings.keyCode;
}
function esc(s) { return s.replace(/[&<>]/g, m => ({'&':'&','<':'<','>':'>'}[m])); }
function fmt(v, d=0) {
if (v === null) return '<span style="color:#569cd6;">null</span>';
if (v === undefined) return '<span style="color:#569cd6;">undefined</span>';
if (typeof v === 'string') return `<span style="color:#ce9178;">"${esc(v)}"</span>`;
if (typeof v === 'number') return `<span style="color:#b5cea8;">${v}</span>`;
if (typeof v === 'boolean') return `<span style="color:#569cd6;">${v}</span>`;
if (typeof v === 'function') return `<span style="color:#dcdcaa;">ƒ ${v.name || 'anon'}()</span>`;
if (Array.isArray(v)) {
if (d > 2) return '<span style="color:#9cdcfe;">Array(...)</span>';
let items = v.slice(0,10).map(x => fmt(x, d+1)).join(',');
return `<span class="exp" data-type="array" data-val='${esc(JSON.stringify(v))}'>▶ Array(${v.length})</span>`;
}
if (typeof v === 'object') {
if (d > 2) return '<span style="color:#9cdcfe;">{...}</span>';
let keys = Object.keys(v).slice(0,5);
let preview = keys.map(k => `${k}: ${fmt(v[k], d+1)}`).join(',');
return `<span class="exp" data-type="object" data-val='${esc(JSON.stringify(v))}'>▶ {${preview}}</span>`;
}
return String(v);
}
function addLog(cmd, res, type='log', isCmd=true) {
if (settings.filter !== 'all' && type !== settings.filter) return;
let entry = document.createElement('div');
entry.className = `log-entry ${type}`;
let time = new Date().toLocaleTimeString();
let resHtml = '';
if (res !== undefined && res !== null) resHtml = `<div class="log-result">${fmt(res)}</div>`;
else if (type === 'info') { resHtml = `<div class="log-result" style="color:#888;">${esc(cmd)}</div>`; cmd = ''; }
else if (type === 'error') { resHtml = `<div class="log-result" style="color:#ff6b6b;">${esc(res || cmd)}</div>`; cmd = ''; }
entry.innerHTML = `${cmd && isCmd ? `<div class="log-cmd"><span style="color:#888;">${time}</span> > ${esc(cmd)}</div>` : ''}${resHtml}`;
logs.appendChild(entry);
entry.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
entry.querySelectorAll('.exp').forEach(el => {
el.onclick = e => {
e.stopPropagation();
if (el.classList.contains('expanded')) {
el.innerHTML = el.getAttribute('data-orig');
el.classList.remove('expanded');
} else {
let data = JSON.parse(el.getAttribute('data-val'));
let expanded = fmt(data, 0).replace('▶', '▼');
el.setAttribute('data-orig', el.innerHTML);
el.innerHTML = expanded;
el.classList.add('expanded');
}
};
});
}
function runCommand(cmd) {
if (!cmd.trim()) return;
if (cmd[0] === '/') { handleSlash(cmd.slice(1)); return; }
settings.history.unshift(cmd);
if (settings.history.length > 100) settings.history.pop();
histIndex = -1;
save();
if (settings.mode === 'c') {
if (!styleTag) { styleTag = document.createElement('style'); styleTag.id = 'vanilla-css'; document.head.appendChild(styleTag); }
styleTag.textContent = cmd;
addLog(cmd, 'CSS applied', 'info');
return;
}
let oldLog = console.log, oldErr = console.error, oldWarn = console.warn, out = null;
console.log = (...a) => { out = a.length === 1 ? a[0] : a; oldLog(...a); };
console.error = (...a) => { out = a[0]; oldErr(...a); };
console.warn = (...a) => { out = a[0]; oldWarn(...a); };
try {
let result = eval(cmd);
if (result !== undefined) out = result;
addLog(cmd, out !== undefined ? out : 'undefined');
} catch(e) { addLog(cmd, e.message, 'error'); }
finally { console.log = oldLog; console.error = oldErr; console.warn = oldWarn; }
}
function handleSlash(arg) {
let parts = arg.trim().split(/\s+/), m = parts[0].toLowerCase();
if (m === 'help') addLog('/help /inspect /clear /theme /pin /reload /js /css /filter [all|log|warn|error]', null, 'info');
else if (m === 'inspect') startInspect();
else if (m === 'clear') { logs.innerHTML = ''; addLog('Logs cleared', null, 'info'); }
else if (m === 'theme') { settings.theme = settings.theme === 'd' ? 'l' : 'd'; save(); applyTheme(); }
else if (m === 'pin') { settings.pin = !settings.pin; save(); pinBtn.textContent = settings.pin ? '📌' : '📍'; }
else if (m === 'reload') location.reload();
else if (m === 'js') { settings.mode = 'j'; modeBtn.textContent = 'JS'; addLog('JS mode', null, 'info'); }
else if (m === 'css') { settings.mode = 'c'; modeBtn.textContent = 'CSS'; addLog('CSS mode', null, 'info'); }
else if (m === 'filter' && parts[1] && ['all','log','warn','error'].includes(parts[1])) { settings.filter = parts[1]; save(); addLog(`Filter: ${settings.filter}`, null, 'info'); document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active')); document.querySelector(`.filter-btn[data-f="${settings.filter}"]`)?.classList.add('active'); }
else addLog(`Unknown: ${m}. /help`, null, 'error');
}
// inspect mode
let inspectActive = false, inspectHighlight, inspectTooltip;
function startInspect() {
if (inspectActive) return;
inspectActive = true;
inspectHighlight = document.createElement('div');
inspectHighlight.style.cssText = 'position:fixed;pointer-events:none;border:2px solid #1e88e5;background:rgba(30,136,229,0.1);z-index:2147483646;display:none;';
inspectTooltip = document.createElement('div');
inspectTooltip.style.cssText = 'position:fixed;background:#1e1e1e;color:#fff;padding:4px 8px;border-radius:6px;font-family:monospace;font-size:11px;z-index:2147483647;pointer-events:none;display:none;';
document.body.appendChild(inspectHighlight);
document.body.appendChild(inspectTooltip);
document.addEventListener('mousemove', onInspectMove);
document.addEventListener('click', onInspectClick, true);
addLog('Inspect mode – click any element', null, 'info');
}
function onInspectMove(e) {
if (!inspectActive) return;
let el = e.target, rect = el.getBoundingClientRect();
inspectHighlight.style.display = 'block';
inspectHighlight.style.top = rect.top + 'px';
inspectHighlight.style.left = rect.left + 'px';
inspectHighlight.style.width = rect.width + 'px';
inspectHighlight.style.height = rect.height + 'px';
let tag = el.tagName.toLowerCase(), id = el.id ? `#${el.id}` : '', cls = el.className ? `.${el.className.split(' ')[0]}` : '';
inspectTooltip.style.display = 'block';
inspectTooltip.style.top = (rect.top - 28) + 'px';
inspectTooltip.style.left = rect.left + 'px';
inspectTooltip.innerHTML = `${tag}${id}${cls}<br>${Math.round(rect.width)}×${Math.round(rect.height)}`;
}
function onInspectClick(e) {
if (!inspectActive) return;
e.preventDefault(); e.stopPropagation();
let el = e.target;
showElementActions(el);
document.removeEventListener('mousemove', onInspectMove);
document.removeEventListener('click', onInspectClick, true);
inspectHighlight.remove(); inspectTooltip.remove();
inspectActive = false;
}
function showElementActions(el) {
let div = document.createElement('div');
div.style.cssText = 'position:fixed;background:#2d2d2d;border:1px solid #555;border-radius:6px;padding:4px;z-index:2147483648;display:flex;gap:4px;';
let rect = el.getBoundingClientRect();
div.style.top = (rect.bottom + 5) + 'px';
div.style.left = rect.left + 'px';
let copy = document.createElement('button'); copy.textContent = 'Copy';
let hide = document.createElement('button'); hide.textContent = 'Hide';
let edit = document.createElement('button'); edit.textContent = 'Edit';
let del = document.createElement('button'); del.textContent = 'Delete';
[copy, hide, edit, del].forEach(b => { b.style.cssText = 'background:#3c3c3c;border:none;color:#fff;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px;'; });
copy.onclick = () => { navigator.clipboard.writeText(getSelector(el)); div.remove(); };
hide.onclick = () => { el.style.display = 'none'; div.remove(); };
edit.onclick = () => { let t = prompt('Edit text:', el.innerText); if (t !== null) el.innerText = t; div.remove(); };
del.onclick = () => { el.remove(); div.remove(); };
div.appendChild(copy); div.appendChild(hide); div.appendChild(edit); div.appendChild(del);
document.body.appendChild(div);
setTimeout(() => div.remove(), 10000);
}
function getSelector(el) {
if (el.id) return `#${el.id}`;
let path = [];
while (el && el.tagName) {
let idx = 1, sib = el.previousElementSibling;
while (sib) { if (sib.tagName === el.tagName) idx++; sib = sib.previousElementSibling; }
path.unshift(el.tagName.toLowerCase() + (idx > 1 ? `:nth-of-type(${idx})` : ''));
el = el.parentElement;
if (path.length > 5) break;
}
return path.join(' > ');
}
// UI theme
function applyTheme() {
let dark = settings.theme === 'd';
let bg = dark ? '#1e1e1e' : '#f3f3f3', fg = dark ? '#ccc' : '#333', ibg = dark ? '#2d2d2d' : '#fff', bd = dark ? '#3c3c3c' : '#ccc';
panel.style.background = bg; panel.style.color = fg; panel.style.borderColor = bd;
input.style.background = ibg; input.style.color = fg; input.style.borderColor = bd;
let style = document.getElementById('vanilla-style');
if (!style) { style = document.createElement('style'); style.id = 'vanilla-style'; document.head.appendChild(style); }
style.textContent = `
#vanilla-panel {
position: fixed; width: 520px; max-width: 90vw; background: ${bg}; border: 1px solid ${bd};
border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.2); font-family: system-ui, monospace;
font-size: ${settings.fontSize}px; display: none; flex-direction: column; z-index: 2147483647;
backdrop-filter: blur(2px); opacity: ${settings.opacity/100};
}
.vanilla-header { padding: 8px 12px; background: ${dark ? '#2d2d2d' : '#e8e8e8'}; border-radius: 8px 8px 0 0; cursor: move; display: flex; justify-content: space-between; user-select: none; }
.vanilla-actions button { background: none; border: none; cursor: pointer; padding: 4px 8px; border-radius: 4px; font-size: 14px; color: ${fg}; opacity: 0.7; }
.vanilla-actions button:hover { background: ${dark ? '#3c3c3c' : '#d0d0d0'}; opacity: 1; }
.filter-bar { display: flex; gap: 6px; padding: 4px 12px; border-bottom: 1px solid ${bd}; }
.filter-btn { background: none; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; color: ${fg}; font-size: 11px; }
.filter-btn.active { background: ${dark ? '#0e6392' : '#007acc'}; color: white; }
.snippets-bar { display: flex; gap: 6px; padding: 6px 12px; border-bottom: 1px solid ${bd}; }
.snippets-bar select, .snippets-bar button { background: ${ibg}; color: ${fg}; border: 1px solid ${bd}; border-radius: 4px; padding: 4px 8px; font-size: 11px; }
.input-line { display: flex; align-items: center; padding: 8px 12px; gap: 8px; border-bottom: 1px solid ${bd}; }
.prompt { font-weight: bold; color: ${dark ? '#9cdcfe' : '#06c'}; }
#vanilla-input { flex: 1; background: ${ibg}; border: 1px solid ${bd}; border-radius: 4px; padding: 6px 8px; color: ${fg}; font-family: monospace; font-size: ${settings.fontSize}px; outline: none; }
.logs-area { height: 260px; overflow-y: auto; padding: 8px; font-family: monospace; font-size: ${settings.fontSize-1}px; }
.log-entry { margin-bottom: 8px; border-bottom: 1px solid ${bd}; padding-bottom: 4px; }
.log-cmd { color: ${dark ? '#9cdcfe' : '#06c'}; word-break: break-all; }
.log-result { padding-left: 16px; color: ${fg}; white-space: pre-wrap; }
.exp { cursor: pointer; color: ${dark ? '#ce9178' : '#a31515'}; }
.resize-handle { position: absolute; bottom: 0; right: 0; width: 15px; height: 15px; cursor: nw-resize; z-index: 10; }
`;
panel.style.opacity = settings.opacity/100;
}
function applyDock() {
if (settings.dock === 'custom') return;
let pos = settings.dock;
panel.style.left = 'auto'; panel.style.right = 'auto'; panel.style.top = 'auto'; panel.style.bottom = 'auto';
if (pos === 'br') { panel.style.bottom = '20px'; panel.style.right = '20px'; }
else if (pos === 'bl') { panel.style.bottom = '20px'; panel.style.left = '20px'; }
else if (pos === 'tr') { panel.style.top = '20px'; panel.style.right = '20px'; }
else if (pos === 'tl') { panel.style.top = '20px'; panel.style.left = '20px'; }
}
function buildUI() {
panel = document.createElement('div'); panel.id = 'vanilla-panel'; document.body.appendChild(panel);
applyTheme(); applyDock();
panel.innerHTML = `
<div class="vanilla-header">
<span>Vanilla</span>
<div class="vanilla-actions">
<button data-act="inspect">⌖</button>
<button data-act="clear">🗑</button>
<button data-act="save">💾</button>
<button data-act="theme">🌓</button>
<button data-act="settings">⚙</button>
<button data-act="pin">${settings.pin ? '📌' : '📍'}</button>
<button data-act="hide">−</button>
</div>
</div>
<div class="filter-bar">
<button class="filter-btn" data-f="all">ALL</button>
<button class="filter-btn" data-f="log">LOG</button>
<button class="filter-btn" data-f="warn">WARN</button>
<button class="filter-btn" data-f="error">ERROR</button>
</div>
<div class="snippets-bar">
<select id="snippet-select"></select>
<button id="snippet-load">Load</button>
<button id="snippet-save">Save</button>
<button id="snippet-del">Del</button>
<button id="mode-switch">${settings.mode === 'j' ? 'JS' : 'CSS'}</button>
</div>
<div class="input-line">
<span class="prompt">></span>
<input type="text" id="vanilla-input" autocomplete="off">
</div>
<div class="logs-area"></div>
<div class="resize-handle"></div>
`;
input = document.getElementById('vanilla-input');
logs = document.querySelector('.logs-area');
pinBtn = document.querySelector('[data-act="pin"]');
modeBtn = document.getElementById('mode-switch');
resizeHandle = document.querySelector('.resize-handle');
// Event listeners
document.querySelectorAll('[data-act]').forEach(btn => {
btn.onclick = () => {
let act = btn.getAttribute('data-act');
if (act === 'inspect') startInspect();
if (act === 'clear') { logs.innerHTML = ''; addLog('Logs cleared', null, 'info'); }
if (act === 'save') saveSnippet();
if (act === 'theme') { settings.theme = settings.theme === 'd' ? 'l' : 'd'; save(); applyTheme(); }
if (act === 'settings') openSettings();
if (act === 'pin') { settings.pin = !settings.pin; save(); pinBtn.textContent = settings.pin ? '📌' : '📍'; }
if (act === 'hide') panel.style.display = 'none';
};
});
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.onclick = () => {
settings.filter = btn.getAttribute('data-f');
save();
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
addLog(`Filter: ${settings.filter}`, null, 'info');
};
if (btn.getAttribute('data-f') === settings.filter) btn.classList.add('active');
});
// Snippets
function refreshSnippets() {
let sel = document.getElementById('snippet-select');
sel.innerHTML = '<option value="">-- snippets --</option>';
for (let name in settings.snippets) {
let opt = document.createElement('option');
opt.value = name; opt.textContent = name;
sel.appendChild(opt);
}
}
document.getElementById('snippet-load').onclick = () => {
let name = document.getElementById('snippet-select').value;
if (name && settings.snippets[name]) input.value = settings.snippets[name];
};
document.getElementById('snippet-save').onclick = saveSnippet;
document.getElementById('snippet-del').onclick = () => {
let name = document.getElementById('snippet-select').value;
if (name && confirm(`Delete "${name}"?`)) {
delete settings.snippets[name];
save(); refreshSnippets();
addLog(`Snippet "${name}" deleted`, null, 'info');
}
};
function saveSnippet() {
let name = prompt('Snippet name:', 'snippet_' + Date.now());
if (name) { settings.snippets[name] = input.value; save(); refreshSnippets(); addLog(`Snippet "${name}" saved`, null, 'info'); }
}
refreshSnippets();
modeBtn.onclick = () => {
settings.mode = settings.mode === 'j' ? 'c' : 'j';
save();
modeBtn.textContent = settings.mode === 'j' ? 'JS' : 'CSS';
addLog(`${settings.mode === 'j' ? 'JS' : 'CSS'} mode`, null, 'info');
};
// Input handling
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
let cmd = input.value;
if (cmd.trim()) runCommand(cmd);
input.value = '';
e.preventDefault();
} else if (e.key === 'ArrowUp') {
if (histIndex + 1 < settings.history.length) {
histIndex++;
input.value = settings.history[histIndex];
}
e.preventDefault();
} else if (e.key === 'ArrowDown') {
if (histIndex > 0) {
histIndex--;
input.value = settings.history[histIndex];
} else if (histIndex === 0) {
histIndex = -1;
input.value = '';
}
e.preventDefault();
}
});
// Drag and resize
let header = document.querySelector('.vanilla-header');
let dragActive = false, dragX, dragY, startLeft, startTop;
header.addEventListener('mousedown', (e) => {
if (e.target.closest('.vanilla-actions')) return;
dragActive = true;
dragX = e.clientX; dragY = e.clientY;
let rect = panel.getBoundingClientRect();
startLeft = rect.left; startTop = rect.top;
panel.style.cursor = 'grabbing';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!dragActive) return;
let left = startLeft + (e.clientX - dragX);
let top = startTop + (e.clientY - dragY);
left = Math.min(Math.max(0, left), window.innerWidth - panel.offsetWidth);
top = Math.min(Math.max(0, top), window.innerHeight - panel.offsetHeight);
panel.style.left = left + 'px';
panel.style.top = top + 'px';
panel.style.right = 'auto';
panel.style.bottom = 'auto';
settings.dock = 'custom';
save();
});
document.addEventListener('mouseup', () => { dragActive = false; panel.style.cursor = ''; });
let resizeActive = false, resizeStartX, resizeStartY, startW, startH;
resizeHandle.addEventListener('mousedown', (e) => {
resizeActive = true;
resizeStartX = e.clientX; resizeStartY = e.clientY;
startW = panel.offsetWidth; startH = panel.offsetHeight;
e.preventDefault(); e.stopPropagation();
});
document.addEventListener('mousemove', (e) => {
if (!resizeActive) return;
let dw = e.clientX - resizeStartX;
let dh = e.clientY - resizeStartY;
let newW = Math.min(Math.max(400, startW + dw), 900);
let newH = Math.min(Math.max(260, startH + dh), 700);
panel.style.width = newW + 'px';
settings.dock = 'custom';
save();
});
document.addEventListener('mouseup', () => { resizeActive = false; });
addLog('Vanilla Console ready', null, 'info');
addLog('Type /help for commands', null, 'info');
}
function openSettings() {
let modal = document.createElement('div');
modal.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#2d2d2d;padding:20px;border-radius:12px;z-index:2147483648;min-width:280px;box-shadow:0 8px 28px rgba(0,0,0,0.4);';
modal.innerHTML = `
<h3 style="margin:0 0 12px;">Settings</h3>
<label>Opacity: <span id="opacityVal">${settings.opacity}</span>%</label>
<input type="range" id="opacitySlider" min="50" max="100" value="${settings.opacity}" style="width:100%;margin:6px 0 12px;">
<label>Font size: <span id="fontSizeVal">${settings.fontSize}</span>px</label>
<input type="range" id="fontSizeSlider" min="10" max="18" value="${settings.fontSize}" style="width:100%;margin:6px 0 12px;">
<label>Dock position:</label>
<select id="dockSelect" style="width:100%;margin:6px 0 12px;">
<option value="br" ${settings.dock==='br'?'selected':''}>Bottom Right</option>
<option value="bl" ${settings.dock==='bl'?'selected':''}>Bottom Left</option>
<option value="tr" ${settings.dock==='tr'?'selected':''}>Top Right</option>
<option value="tl" ${settings.dock==='tl'?'selected':''}>Top Left</option>
<option value="custom" ${settings.dock==='custom'?'selected':''}>Custom (draggable)</option>
</select>
<label>Key modifier:</label>
<select id="keyModSelect" style="width:100%;margin:6px 0 12px;">
<option value="Alt" ${settings.keyMod==='Alt'?'selected':''}>Alt</option>
<option value="Ctrl" ${settings.keyMod==='Ctrl'?'selected':''}>Ctrl</option>
<option value="Shift" ${settings.keyMod==='Shift'?'selected':''}>Shift</option>
<option value="Meta" ${settings.keyMod==='Meta'?'selected':''}>${isMac?'Cmd':'Win'}</option>
</select>
<label>Key letter:</label>
<input type="text" id="keyCodeInput" placeholder="e.g., KeyX" value="${settings.keyCode}" style="width:100%;margin:6px 0 12px;padding:6px;border-radius:4px;">
<div style="margin-top:12px;">
<label><input type="checkbox" id="pinCheck" ${settings.pin?'checked':''}> Pin mode</label>
</div>
<div style="display:flex;justify-content:flex-end;margin-top:16px;">
<button id="closeModal" style="padding:6px 12px;">Close</button>
</div>
`;
document.body.appendChild(modal);
let opSlider = modal.querySelector('#opacitySlider'), fsSlider = modal.querySelector('#fontSizeSlider'), dockSel = modal.querySelector('#dockSelect'), modSel = modal.querySelector('#keyModSelect'), keyInp = modal.querySelector('#keyCodeInput'), pinChk = modal.querySelector('#pinCheck');
opSlider.oninput = () => { settings.opacity = +opSlider.value; document.getElementById('opacityVal').innerText = settings.opacity; panel.style.opacity = settings.opacity/100; save(); };
fsSlider.oninput = () => { settings.fontSize = +fsSlider.value; document.getElementById('fontSizeVal').innerText = settings.fontSize; applyTheme(); save(); };
dockSel.onchange = () => { settings.dock = dockSel.value; applyDock(); save(); };
modSel.onchange = () => { settings.keyMod = modSel.value; save(); updateKeybind(); };
keyInp.onchange = () => { let val = keyInp.value.trim(); if (val) { settings.keyCode = val; save(); updateKeybind(); } };
pinChk.onchange = () => { settings.pin = pinChk.checked; save(); pinBtn.textContent = settings.pin ? '📌' : '📍'; };
modal.querySelector('#closeModal').onclick = () => modal.remove();
}
function updateKeybind() {
// remove old listener and add new one
document.removeEventListener('keydown', globalKeyHandler);
document.addEventListener('keydown', globalKeyHandler);
}
function globalKeyHandler(e) {
let mod = false;
if (settings.keyMod === 'Alt') mod = e.altKey;
else if (settings.keyMod === 'Ctrl') mod = e.ctrlKey;
else if (settings.keyMod === 'Shift') mod = e.shiftKey;
else if (settings.keyMod === 'Meta') mod = e.metaKey;
if (mod && e.code === settings.keyCode) {
e.preventDefault();
if (panel.style.display === 'none') panel.style.display = 'flex';
else panel.style.display = 'none';
}
// Command palette Cmd/Ctrl+K
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
showCommandPalette();
}
}
function showCommandPalette() {
let cp = document.createElement('div');
cp.style.cssText = 'position:fixed;top:30%;left:50%;transform:translate(-50%,-50%);background:#2d2d2d;border-radius:12px;width:400px;z-index:2147483649;box-shadow:0 10px 40px rgba(0,0,0,0.5);';
cp.innerHTML = `<input type="text" id="cp-input" placeholder="Run command... (/inspect, /clear, etc.)" style="width:calc(100% - 24px);margin:12px;padding:8px;background:#1e1e1e;border:1px solid #555;border-radius:6px;color:#fff;font-family:monospace;">
<div id="cp-list" style="max-height:300px;overflow-y:auto;padding:0 12px 12px 12px;"></div>`;
document.body.appendChild(cp);
let inp = cp.querySelector('#cp-input');
let listDiv = cp.querySelector('#cp-list');
let cmds = ['/help', '/inspect', '/clear', '/theme', '/pin', '/reload', '/js', '/css', '/filter all', '/filter log', '/filter warn', '/filter error'];
function render(filter='') {
listDiv.innerHTML = '';
cmds.filter(c => c.includes(filter)).forEach(cmd => {
let div = document.createElement('div');
div.textContent = cmd;
div.style.padding = '6px'; div.style.cursor = 'pointer'; div.style.borderRadius = '4px';
div.onmouseenter = () => div.style.background = '#3c3c3c';
div.onmouseleave = () => div.style.background = '';
div.onclick = () => { runCommand(cmd); cp.remove(); };
listDiv.appendChild(div);
});
}
render();
inp.oninput = () => render(inp.value);
inp.onkeydown = (e) => {
if (e.key === 'Enter' && inp.value) { runCommand(inp.value); cp.remove(); }
if (e.key === 'Escape') cp.remove();
};
inp.focus();
}
function init() {
buildUI();
document.addEventListener('keydown', globalKeyHandler);
// start hidden
panel.style.display = 'none';
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();