Floating companion panel for Script Forge. Bypass links in one click.
// ==UserScript==
// @name Script Forge
// @namespace https://script-forge.xyz
// @version 1.0.0
// @description Floating companion panel for Script Forge. Bypass links in one click.
// @author Script Forge
// @license All rights reserved
// @match *://*/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_openInTab
// @run-at document-end
// @noframes
// ==/UserScript==
(function () {
'use strict';
const SITE = 'https://script-forge.xyz';
const VERSION = '1.0.0';
if (/^(www\.)?script-forge\.xyz$/.test(location.hostname)) return;
const KEYS = { x: 'sf_x', y: 'sf_y', collapsed: 'sf_col' };
const Store = {
get(k, fb) {
try { const v = GM_getValue(k, null); return v !== null ? v : fb; } catch { return fb; }
},
set(k, v) { try { GM_setValue(k, v); } catch { } }
};
const Bus = (() => {
const m = {};
return {
on(e, fn) { (m[e] ??= []).push(fn); },
off(e, fn) { m[e] = (m[e] || []).filter(h => h !== fn); },
emit(e, d) { (m[e] || []).forEach(fn => fn(d)); }
};
})();
const Registry = (() => {
const mods = new Map();
return {
add(id, mod) { mods.set(id, mod); },
all() { return [...mods.values()]; }
};
})();
function makeDraggable(wrap, handle, onEnd, threshold = 4) {
let ox, oy, sx, sy, moved = false, live = false, raf;
function clampedPos(nx, ny) {
return {
x: Math.max(0, Math.min(nx, window.innerWidth - wrap.offsetWidth)),
y: Math.max(0, Math.min(ny, window.innerHeight - wrap.offsetHeight))
};
}
function applyPos(x, y) {
wrap.style.left = x + 'px';
wrap.style.top = y + 'px';
wrap.style.right = 'auto';
wrap.style.bottom = 'auto';
}
function onDown(e) {
if (e.button !== 0) return;
if (e.target.closest('[data-action]')) return;
live = true;
moved = false;
const r = wrap.getBoundingClientRect();
ox = r.left; oy = r.top;
sx = e.clientX; sy = e.clientY;
handle.setPointerCapture(e.pointerId);
e.preventDefault();
}
function onMove(e) {
if (!live) return;
const dx = e.clientX - sx, dy = e.clientY - sy;
if (!moved && dx * dx + dy * dy < threshold * threshold) return;
moved = true;
cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => {
const p = clampedPos(ox + dx, oy + dy);
applyPos(p.x, p.y);
});
}
function onUp() {
if (!live) return;
live = false;
cancelAnimationFrame(raf);
if (moved && onEnd) {
const r = wrap.getBoundingClientRect();
onEnd(r.left, r.top);
}
}
handle.addEventListener('pointerdown', onDown);
handle.addEventListener('pointermove', onMove);
handle.addEventListener('pointerup', onUp);
handle.addEventListener('pointercancel', onUp);
return {
didMove() { return moved; },
destroy() {
handle.removeEventListener('pointerdown', onDown);
handle.removeEventListener('pointermove', onMove);
handle.removeEventListener('pointerup', onUp);
handle.removeEventListener('pointercancel', onUp);
}
};
}
const CSS = `
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
#w {
position: fixed;
z-index: 2147483647;
}
#panel {
width: 272px;
background: #111111;
border: 1px solid #1e1e1e;
border-radius: 6px;
box-shadow:
0 16px 48px rgba(0,0,0,.7),
0 1px 0 rgba(255,255,255,.03) inset;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
overflow: hidden;
display: none;
}
#panel.on {
display: block;
animation: p-in .14s ease;
}
@keyframes p-in {
from { opacity: 0; transform: translateY(5px) scale(.985); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.hd {
display: flex;
align-items: center;
height: 40px;
padding: 0 8px 0 14px;
background: #0d0d0d;
border-bottom: 1px solid #191919;
cursor: grab;
user-select: none;
}
.hd:active { cursor: grabbing; }
.brand {
flex: 1;
font-size: 12px;
font-weight: 600;
letter-spacing: .01em;
color: #888888;
}
.brand b {
font-weight: 600;
color: #e05a28;
}
.ctrls { display: flex; }
.ib {
all: unset;
box-sizing: border-box;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
color: #3c3c3c;
transition: color .1s, background .1s;
}
.ib:hover { color: #888; background: rgba(255,255,255,.05); }
.ib:active { color: #aaa; }
.ib svg { display: block; }
.bd { padding: 14px; }
.lbl {
font-size: 9px;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
color: #2e2e2e;
margin-bottom: 6px;
}
.url-wrap {
padding: 8px 10px;
background: #0b0b0b;
border: 1px solid #1a1a1a;
border-radius: 4px;
font-family: 'SF Mono', 'JetBrains Mono', 'Fira Code', Consolas, monospace;
font-size: 10.5px;
color: #3e3e3e;
word-break: break-all;
line-height: 1.6;
margin-bottom: 10px;
min-height: 38px;
}
.url-wrap.has { color: #747474; }
.go {
all: unset;
box-sizing: border-box;
display: block;
width: 100%;
padding: 9px 14px;
background: #e05a28;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
color: #fff;
text-align: center;
cursor: pointer;
letter-spacing: .005em;
transition: background .1s;
}
.go:hover { background: #c94d21; }
.go:active { background: #b3451e; }
.ft {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 14px;
border-top: 1px solid #171717;
}
.ft-a { font-size: 10px; color: #272727; }
.ft-b { font-size: 9px; color: #232323; font-variant-numeric: tabular-nums; }
#tab {
display: none;
align-items: center;
gap: 8px;
height: 34px;
padding: 0 13px;
background: #111111;
border: 1px solid #1e1e1e;
border-radius: 5px;
box-shadow: 0 6px 22px rgba(0,0,0,.6);
cursor: grab;
user-select: none;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
}
#tab.on { display: flex; }
#tab:active { cursor: grabbing; }
.t-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: #e05a28;
flex-shrink: 0;
}
.t-lbl {
font-size: 11.5px;
font-weight: 600;
color: #555555;
letter-spacing: .01em;
white-space: nowrap;
}
`;
function bypassMod() {
let urlEl = null;
function getUrl() { return location.href; }
function getDisp(url) { return url.length > 60 ? url.slice(0, 58) + '\u2026' : url; }
function refreshUrl() {
if (!urlEl) return;
const url = getUrl();
const disp = getDisp(url);
urlEl.textContent = disp || '\u2014';
urlEl.className = 'url-wrap' + (url ? ' has' : '');
}
return {
id: 'bypass',
render() {
const url = getUrl();
const disp = getDisp(url);
const el = document.createElement('div');
el.className = 'bd';
el.innerHTML =
'<div class="lbl">Current Page</div>' +
'<div class="url-wrap' + (url ? ' has' : '') + '">' + (disp || '\u2014') + '</div>' +
'<button class="go" data-action="bypass">Bypass with Script Forge</button>';
urlEl = el.querySelector('.url-wrap');
return el;
},
mount(root) {
root.querySelector('[data-action="bypass"]').addEventListener('click', () => {
GM_openInTab(SITE + '/?url=' + encodeURIComponent(getUrl()), { active: true, insert: true });
});
// Keep display in sync with SPA / client-side navigation
let _lastUrl = location.href;
const _nav = setInterval(() => {
if (location.href !== _lastUrl) {
_lastUrl = location.href;
refreshUrl();
}
}, 500);
// Also listen for popstate / hashchange
window.addEventListener('popstate', refreshUrl, { passive: true });
window.addEventListener('hashchange', refreshUrl, { passive: true });
}
};
}
function buildWidget() {
const collapsed = Store.get(KEYS.collapsed, false);
const px = Store.get(KEYS.x, null);
const py = Store.get(KEYS.y, null);
const host = document.createElement('div');
const root = host.attachShadow({ mode: 'open' });
document.documentElement.appendChild(host);
const style = document.createElement('style');
style.textContent = CSS;
const wrap = document.createElement('div');
wrap.id = 'w';
if (px !== null && py !== null) {
wrap.style.left = px + 'px';
wrap.style.top = py + 'px';
} else {
wrap.style.bottom = '20px';
wrap.style.right = '20px';
}
const panel = document.createElement('div');
panel.id = 'panel';
const hd = document.createElement('div');
hd.className = 'hd';
hd.innerHTML =
'<div class="brand"><b>Script</b> Forge</div>' +
'<div class="ctrls">' +
'<button class="ib" data-action="collapse" title="Minimise">' +
'<svg width="10" height="1.5" viewBox="0 0 10 1.5">' +
'<rect width="10" height="1.5" rx=".75" fill="currentColor"/>' +
'</svg>' +
'</button>' +
'</div>';
panel.appendChild(hd);
const mods = Registry.all();
mods.forEach(m => panel.appendChild(m.render()));
const ft = document.createElement('div');
ft.className = 'ft';
ft.innerHTML =
'<span class="ft-a">script-forge.xyz</span>' +
'<span class="ft-b">v' + VERSION + '</span>';
panel.appendChild(ft);
const tab = document.createElement('div');
tab.id = 'tab';
tab.innerHTML =
'<div class="t-lbl">Script Forge</div>';
wrap.appendChild(panel);
wrap.appendChild(tab);
root.appendChild(style);
root.appendChild(wrap);
function savePos() {
const r = wrap.getBoundingClientRect();
Store.set(KEYS.x, r.left);
Store.set(KEYS.y, r.top);
}
function clamp() {
const r = wrap.getBoundingClientRect();
const cx = Math.max(0, Math.min(r.left, window.innerWidth - wrap.offsetWidth));
const cy = Math.max(0, Math.min(r.top, window.innerHeight - wrap.offsetHeight));
wrap.style.left = cx + 'px';
wrap.style.top = cy + 'px';
wrap.style.right = 'auto';
wrap.style.bottom = 'auto';
}
function openPanel() {
tab.classList.remove('on');
panel.classList.add('on');
Store.set(KEYS.collapsed, false);
requestAnimationFrame(clamp);
}
function openTab() {
panel.classList.remove('on');
tab.classList.add('on');
Store.set(KEYS.collapsed, true);
requestAnimationFrame(clamp);
}
if (collapsed) openTab();
else openPanel();
requestAnimationFrame(() => {
if (px === null || py === null) {
const r = wrap.getBoundingClientRect();
wrap.style.left = r.left + 'px';
wrap.style.top = r.top + 'px';
wrap.style.right = 'auto';
wrap.style.bottom = 'auto';
}
clamp();
});
makeDraggable(wrap, hd, savePos);
const tabDrag = makeDraggable(wrap, tab, savePos);
root.querySelector('[data-action="collapse"]').addEventListener('click', e => {
e.stopPropagation();
openTab();
});
tab.addEventListener('click', () => {
if (!tabDrag.didMove()) openPanel();
});
mods.forEach(m => { if (m.mount) m.mount(root); });
window.addEventListener('resize', clamp, { passive: true });
}
Registry.add('bypass', bypassMod());
buildWidget();
})();