OScope: every text file of commit 9d9eae4d711c8575b6fbb4d882cc53b3af7dc798, part 3 of 3. Code: https://git.oscope.trade/oscope · git clone https://git.oscope.trade/oscope.git · manifest: https://git.oscope.trade/oscope/manifest.json Each file starts with "===== FILE: =====" and a line with its sha256, size and line count. Nothing is left out or changed. ===== FILE: public/js/util.js ===== sha256: ea7a16c0ef7b926a5ba8afb100d93afeb1d8dabfd60551bb792e37c506a7b912 · bytes: 4796 · lines: 112 // Small helpers shared by every view. export const $ = (sel, root = document) => root.querySelector(sel); export const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; // h('div.row.active', { onclick, title }, child, 'text', ...) export function h(tag, attrs, ...kids) { const [name, ...cls] = tag.split('.'); const el = document.createElement(name || 'div'); if (cls.length) el.className = cls.join(' '); if (attrs) { for (const [k, v] of Object.entries(attrs)) { if (v == null || v === false) continue; if (k.startsWith('on')) el.addEventListener(k.slice(2), v); else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v); else if (k === 'html') el.innerHTML = v; // only ever used with our own markup (icons) else if (k in el && k !== 'list' && k !== 'type') el[k] = v; else el.setAttribute(k, v === true ? '' : v); } } for (const k of kids.flat()) if (k != null && k !== false) el.append(k instanceof Node ? k : document.createTextNode(String(k))); return el; } export const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); export function compact(v, digits = 1) { if (!Number.isFinite(v)) return '–'; const a = Math.abs(v); if (a >= 1e12) return (v / 1e12).toFixed(digits) + 'T'; if (a >= 1e9) return (v / 1e9).toFixed(digits) + 'B'; if (a >= 1e6) return (v / 1e6).toFixed(digits) + 'M'; if (a >= 1e3) return (v / 1e3).toFixed(digits) + 'K'; if (a >= 100) return v.toFixed(0); if (a >= 1) return v.toFixed(a >= 10 ? 1 : 2); if (a === 0) return '0'; return v.toFixed(a >= 0.01 ? 2 : a >= 0.001 ? 3 : 4); } export const count = n => (Number.isFinite(n) ? (Math.abs(n) < 1000 ? String(Math.round(n)) : compact(n, Math.abs(n) < 1e4 ? 1 : 0).replace('.0K', 'K')) : '–'); export const usd = v => (v < 0 ? '-$' : '$') + compact(Math.abs(v)); export function sol(v, d) { if (!Number.isFinite(v)) return '–'; const a = Math.abs(v); const digits = d ?? (a >= 1000 ? 0 : a >= 100 ? 1 : a >= 1 ? 2 : a >= 0.01 ? 3 : 4); return a >= 10000 ? compact(v) : v.toFixed(digits); } export const pct = (v, d = 1) => (Number.isFinite(v) ? (+v).toFixed(v !== 0 && Math.abs(v) < 1 && d < 2 ? 2 : d) + '%' : '–'); export const signed = (v, f) => (v > 0 ? '+' : v < 0 ? '−' : '') + f(Math.abs(v)); // price of one token in dollars: many zeros are written as 0.0₅123 export function tinyUsd(v) { if (!Number.isFinite(v) || v <= 0) return '–'; if (v >= 1) return '$' + v.toFixed(v >= 100 ? 2 : 4); const s = v.toFixed(20).slice(2); const zeros = s.match(/^0*/)[0].length; if (zeros < 4) return '$' + v.toFixed(zeros + 4); const sub = String(zeros).split('').map(d => '₀₁₂₃₄₅₆₇₈₉'[d]).join(''); return '$0.0' + sub + s.slice(zeros, zeros + 4).replace(/0+$/, ''); } export function age(ts, nowMs = Date.now()) { if (!ts) return '–'; const s = Math.max(0, Math.floor((nowMs - ts) / 1000)); if (s < 60) return s + 's'; const m = Math.floor(s / 60); if (m < 60) return m + 'm'; const hh = Math.floor(m / 60); if (hh < 24) return hh + 'h'; const d = Math.floor(hh / 24); return d < 30 ? d + 'd' : Math.floor(d / 30) + 'mo'; } export const short = (a, n = 4) => (a ? a.slice(0, n) + '…' + a.slice(-n) : ''); export function copy(text, label = 'Copied') { navigator.clipboard?.writeText(text).then(() => toast(label, 'info', 1400)).catch(() => {}); } let toastRoot; export function toast(text, kind = 'info', ms = 3000, node) { toastRoot ||= document.getElementById('toasts'); const t = h('div.toast.' + kind, null, node || h('span', null, text)); toastRoot.prepend(t); while (toastRoot.children.length > 5) toastRoot.lastChild.remove(); requestAnimationFrame(() => t.classList.add('in')); setTimeout(() => { t.classList.remove('in'); setTimeout(() => t.remove(), 250); }, ms); return t; } export function safeHref(u) { try { const x = new URL(u); return x.protocol === 'https:' || x.protocol === 'http:' ? x.href : null; } catch { return null; } } export const store = { get(k, d) { try { const v = localStorage.getItem('oscope.' + k); return v == null ? d : JSON.parse(v); } catch { return d; } }, set(k, v) { try { localStorage.setItem('oscope.' + k, JSON.stringify(v)); } catch {} }, }; // a deterministic colour for a coin with no picture export function hue(str) { let x = 0; for (let i = 0; i < str.length; i++) x = (x * 31 + str.charCodeAt(i)) >>> 0; return x % 360; } export function emitter() { const m = new Map(); return { on(e, f) { if (!m.has(e)) m.set(e, new Set()); m.get(e).add(f); return () => m.get(e).delete(f); }, emit(e, ...a) { for (const f of m.get(e) || []) f(...a); }, }; } ===== FILE: public/index.html ===== sha256: af00a42a5ee4c6f9d8a6cc947771dd8cf337582fc21836a384691bb68c9b5d3b · bytes: 1950 · lines: 47 OScope
OScope
Connecting
SOL–
–Paper
===== FILE: public/css/app.css ===== sha256: ad5af6aaf02e57eb042b6016f5d2f23eb6e95f321109eaa53ecb26c7347eb37b · bytes: 29223 · lines: 378 @font-face { font-family: 'Geist'; src: url('/fonts/Geist-Variable.woff2') format('woff2'); font-weight: 100 900; font-display: swap; } @font-face { font-family: 'Geist Mono'; src: url('/fonts/GeistMono-Variable.woff2') format('woff2'); font-weight: 100 900; font-display: swap; } :root { --bg: #06070a; --bg1: #0a0c10; --bg2: #0f1117; --bg3: #141720; --bg4: #1b1f2a; --line: #171a22; --line2: #222733; --line3: #2e3442; --tx: #eceff5; --tx2: #a9afbd; --tx3: #737b8c; --tx4: #4c5363; --acc: #4d8dff; --acc2: #2f6bff; --acc-bg: rgba(77, 141, 255, .11); --acc-bg2: rgba(77, 141, 255, .2); --up: #2fe39b; --up-bg: rgba(47, 227, 155, .1); --down: #ff5d7b; --down-bg: rgba(255, 93, 123, .1); --warn: #ffbf57; --warn-bg: rgba(255, 191, 87, .1); --gold: #f2c96b; --r: 6px; --r2: 4px; --f: 'Geist', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif; --fm: 'Geist Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace; --top: 48px; color-scheme: dark; } * { box-sizing: border-box; } html, body { margin: 0; height: 100%; background: var(--bg); color: var(--tx); font: 13px/1.35 var(--f); -webkit-font-smoothing: antialiased; } body { overflow: hidden; font-feature-settings: 'tnum' 1, 'cv11' 1; } a { color: inherit; text-decoration: none; } button, input, select, textarea { font: inherit; color: inherit; } button { cursor: pointer; background: none; border: 0; padding: 0; } .ic { display: block; flex: none; } .mono { font-family: var(--fm); font-size: .92em; letter-spacing: -.01em; } .up { color: var(--up); } .down { color: var(--down); } .mut { color: var(--tx3); } .warn { color: var(--warn); } ::selection { background: var(--acc-bg2); } ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-thumb { background: var(--line2); border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; } ::-webkit-scrollbar-thumb:hover { background-color: var(--line3); } ::-webkit-scrollbar-track { background: transparent; } /* ---------- header ---------- */ .top { position: fixed; inset: 0 0 auto 0; height: var(--top); display: flex; align-items: center; gap: 18px; padding: 0 14px 0 16px; background: rgba(8, 9, 13, .92); border-bottom: 1px solid var(--line); z-index: 20; backdrop-filter: blur(10px); } .brand { display: flex; align-items: center; gap: 9px; font-weight: 650; font-size: 15px; letter-spacing: -.02em; } .brand img { display: block; } .nav { display: flex; gap: 2px; margin-left: 6px; } .nav a { height: 30px; display: flex; align-items: center; padding: 0 11px; border-radius: var(--r); color: var(--tx3); font-weight: 500; transition: color .15s, background .15s; } .nav a:hover { color: var(--tx); } .nav a.on { color: var(--tx); background: var(--bg3); } .search { position: relative; display: flex; align-items: center; height: 32px; width: 340px; margin-left: 8px; padding: 0 8px 0 10px; gap: 8px; background: var(--bg2); border: 1px solid var(--line2); border-radius: var(--r); transition: border-color .15s, background .15s; } .search:focus-within { border-color: var(--line3); background: var(--bg3); } .search-ic { color: var(--tx4); } .search input { flex: 1; min-width: 0; border: 0; outline: 0; background: none; font-size: 13px; } .search input::placeholder { color: var(--tx4); } kbd { font: 500 11px var(--fm); color: var(--tx4); border: 1px solid var(--line2); border-radius: 4px; padding: 1px 5px; } .search-res { position: absolute; top: 38px; left: 0; width: 460px; max-height: 70vh; overflow: auto; background: var(--bg2); border: 1px solid var(--line2); border-radius: 8px; box-shadow: 0 20px 50px rgba(0, 0, 0, .55); display: none; padding: 4px; } .search-res.open { display: block; } .sr { display: grid; grid-template-columns: 36px 1fr auto; gap: 10px; align-items: center; padding: 7px 8px; border-radius: var(--r); } .sr:hover, .sr.sel { background: var(--bg3); } .sr .av { width: 36px; height: 36px; } .sr b { font-weight: 600; } .sr .mut { font-size: 12px; } .sr-empty { padding: 14px; color: var(--tx3); text-align: center; } .top-right { margin-left: auto; display: flex; align-items: center; gap: 8px; } .feed { display: flex; align-items: center; gap: 7px; height: 28px; padding: 0 10px; color: var(--tx3); font-size: 12px; font-weight: 500; } .feed i { width: 7px; height: 7px; border-radius: 50%; background: var(--tx4); box-shadow: 0 0 0 3px rgba(255, 255, 255, .04); } .feed.ok i { background: var(--up); box-shadow: 0 0 0 3px var(--up-bg), 0 0 10px rgba(47, 227, 155, .5); animation: beat 2.4s ease-in-out infinite; } .feed.bad i { background: var(--down); box-shadow: 0 0 0 3px var(--down-bg); } @keyframes beat { 50% { box-shadow: 0 0 0 5px rgba(47, 227, 155, .05), 0 0 14px rgba(47, 227, 155, .35); } } .solpx em { font-style: normal; font-size: 11px; font-weight: 650; color: var(--tx4); } .solpx { display: flex; align-items: center; gap: 6px; height: 30px; padding: 0 10px; border-radius: var(--r); background: var(--bg2); border: 1px solid var(--line); font-weight: 600; font-size: 12.5px; } .wallet { display: flex; align-items: center; gap: 7px; height: 30px; padding: 0 6px 0 10px; border-radius: var(--r); background: var(--bg2); border: 1px solid var(--line); font-size: 12.5px; transition: border-color .15s; } .wallet:hover { border-color: var(--line3); } .wallet-ic { color: var(--tx3); } .wallet b { font-weight: 600; } .wallet em { font-style: normal; font-size: 10.5px; font-weight: 600; letter-spacing: .02em; color: var(--warn); background: var(--warn-bg); border-radius: 4px; padding: 2px 6px; } .icon-btn { width: 30px; height: 30px; display: grid; place-items: center; border-radius: var(--r); color: var(--tx3); transition: color .15s, background .15s; } .icon-btn:hover { color: var(--tx); background: var(--bg3); } .icon-btn.on { color: var(--acc); background: var(--acc-bg); } #view { position: fixed; inset: var(--top) 0 0 0; overflow: hidden; } .page { height: 100%; overflow: auto; } .page-in { animation: pageIn .22s ease-out; } @keyframes pageIn { from { opacity: 0; transform: translateY(4px); } } /* ---------- common bits ---------- */ .av { position: relative; width: 64px; height: 64px; border-radius: 8px; overflow: hidden; flex: none; background: var(--bg3); } .av img { width: 100%; height: 100%; object-fit: cover; display: block; opacity: 0; transition: opacity .25s; } .av img.ok { opacity: 1; } .av .ph { position: absolute; inset: 0; display: grid; place-items: center; font-weight: 700; font-size: 20px; color: rgba(255, 255, 255, .85); letter-spacing: -.03em; } .chip { display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 6px; border-radius: 4px; font-size: 11.5px; font-weight: 550; color: var(--tx2); background: var(--bg3); white-space: nowrap; } .chip.lv-g { color: var(--up); background: var(--up-bg); } .chip.lv-b { color: var(--down); background: var(--down-bg); } .chip.lv-m { color: var(--warn); background: var(--warn-bg); } .chip .ic { opacity: .9; } .tag { display: inline-flex; align-items: center; height: 17px; padding: 0 5px; border-radius: 3px; font-size: 10px; font-weight: 650; letter-spacing: .03em; text-transform: uppercase; vertical-align: 1px; } .tag.dev { color: #ffd479; background: rgba(255, 212, 121, .12); } .tag.sniper { color: #ff8db5; background: rgba(255, 141, 181, .12); } .tag.bundle { color: #c7a4ff; background: rgba(199, 164, 255, .12); } .tag.you { color: var(--acc); background: var(--acc-bg); } .tag.tracked { color: #8fe3b8; background: rgba(143, 227, 184, .12); } .tag.mig { color: var(--gold); background: rgba(242, 201, 107, .12); } .btn { height: 32px; padding: 0 14px; border-radius: var(--r); background: var(--bg3); border: 1px solid var(--line2); font-weight: 550; display: inline-flex; align-items: center; justify-content: center; gap: 7px; transition: background .15s, border-color .15s, color .15s; white-space: nowrap; } .btn:hover { background: var(--bg4); border-color: var(--line3); } .btn.acc { background: var(--acc); color: #fff; border-color: transparent; } .btn.acc:hover { background: #6aa2ff; } .btn.ghost { background: none; border-color: transparent; color: var(--tx3); } .btn.ghost:hover { color: var(--tx); background: var(--bg3); } .btn.sm { height: 26px; padding: 0 9px; font-size: 12px; } .btn.danger:hover { color: var(--down); border-color: rgba(255, 93, 123, .35); } .inp { height: 32px; padding: 0 10px; border-radius: var(--r); background: var(--bg2); border: 1px solid var(--line2); outline: 0; min-width: 0; transition: border-color .15s; } .inp:focus { border-color: var(--acc2); } .inp::placeholder { color: var(--tx4); } .lbl { font-size: 12px; color: var(--tx3); font-weight: 500; } .empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 48px 20px; color: var(--tx3); text-align: center; } .empty b { color: var(--tx2); font-weight: 600; font-size: 14px; } .trace { width: 120px; height: 32px; color: var(--acc); opacity: .7; } .trace path { stroke-dasharray: 160; animation: trace 2.2s linear infinite; } @keyframes trace { from { stroke-dashoffset: 320; } to { stroke-dashoffset: 0; } } /* ---------- toasts ---------- */ #toasts { position: fixed; right: 16px; bottom: 16px; display: flex; flex-direction: column; gap: 8px; z-index: 50; pointer-events: none; } .toast { pointer-events: auto; min-width: 240px; max-width: 420px; padding: 10px 14px; border-radius: 8px; background: rgba(20, 23, 32, .97); border: 1px solid var(--line2); box-shadow: 0 16px 40px rgba(0, 0, 0, .5); font-size: 13px; opacity: 0; transform: translateY(8px); transition: opacity .2s, transform .2s; border-left: 3px solid var(--line3); } .toast.in { opacity: 1; transform: none; } .toast.buy { border-left-color: var(--up); } .toast.sell { border-left-color: var(--down); } .toast.warn { border-left-color: var(--warn); } .toast.info { border-left-color: var(--acc); } .tt { display: block; } .tt-who { font-weight: 650; } /* ---------- modal ---------- */ .modal-back { position: fixed; inset: 0; background: rgba(3, 4, 7, .7); backdrop-filter: blur(3px); z-index: 40; display: grid; place-items: center; animation: fade .15s; } @keyframes fade { from { opacity: 0; } } .modal { width: min(520px, calc(100vw - 32px)); max-height: calc(100vh - 64px); overflow: auto; background: var(--bg1); border: 1px solid var(--line2); border-radius: 12px; box-shadow: 0 30px 80px rgba(0, 0, 0, .6); animation: pop .18s ease-out; } @keyframes pop { from { transform: scale(.98) translateY(6px); opacity: 0; } } .modal-h { display: flex; align-items: center; justify-content: space-between; padding: 16px 18px; border-bottom: 1px solid var(--line); } .modal-h h3 { margin: 0; font-size: 15px; font-weight: 650; } .modal-b { padding: 16px 18px 18px; display: grid; gap: 16px; } .field { display: grid; gap: 6px; } .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .switch { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .tg { width: 34px; height: 20px; border-radius: 20px; background: var(--bg4); position: relative; transition: background .15s; flex: none; } .tg::after { content: ''; position: absolute; top: 3px; left: 3px; width: 14px; height: 14px; border-radius: 50%; background: var(--tx3); transition: transform .15s, background .15s; } .tg.on { background: var(--acc2); } .tg.on::after { transform: translateX(14px); background: #fff; } /* ---------- pulse ---------- */ .pulse { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); height: 100%; } .col { display: flex; flex-direction: column; min-width: 0; border-right: 1px solid var(--line); background: var(--bg); } .col:last-child { border-right: 0; } .col-h { flex: none; height: 46px; display: flex; align-items: center; gap: 10px; padding: 0 10px 0 16px; border-bottom: 1px solid var(--line); background: var(--bg1); } .col-h h2 { margin: 0; font-size: 14px; font-weight: 650; letter-spacing: -.01em; } .count:empty { display: none; } .count { font: 600 11px var(--fm); color: var(--tx3); background: var(--bg3); border-radius: 4px; padding: 2px 6px; } .paused { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; color: var(--warn); opacity: 0; transition: opacity .15s; } .col.hold .paused { opacity: 1; } .col-tools { margin-left: auto; display: flex; align-items: center; gap: 4px; } .qb-set { display: flex; align-items: center; gap: 5px; height: 28px; padding: 0 8px; border-radius: var(--r); background: var(--bg2); border: 1px solid var(--line2); color: var(--acc); } .qb-set input { width: 42px; border: 0; outline: 0; background: none; font-weight: 600; font-size: 12.5px; color: var(--tx); } .qb-unit { font-size: 11px; font-weight: 600; color: var(--tx4); } .list { flex: 1; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; } .row { position: relative; display: grid; grid-template-columns: 68px minmax(0, 1fr) auto; gap: 12px; padding: 12px 14px 12px 14px; border-bottom: 1px solid var(--line); cursor: pointer; transition: background .12s; content-visibility: auto; contain-intrinsic-size: auto 93px; height: 93px; } .row:hover { background: var(--bg2); } .row.new { animation: rowIn 1.6s ease-out; } @keyframes rowIn { 0% { background: rgba(77, 141, 255, .1); } 100% { background: transparent; } } .ring { position: relative; width: 68px; height: 68px; padding: 2px; border-radius: 10px; background: conic-gradient(var(--ringc, var(--acc)) calc(var(--p, 0) * 1%), var(--line2) 0); } .ring.mig { --ringc: var(--gold); background: var(--gold); } .ring .av { border: 2px solid var(--bg); border-radius: 8px; width: 64px; height: 64px; } .row:hover .ring .av { border-color: var(--bg2); } .badge { position: absolute; right: -3px; bottom: -3px; width: 18px; height: 18px; border-radius: 50%; display: grid; place-items: center; background: var(--bg1); border: 1.5px solid var(--line3); color: var(--acc); } .ring.mig .badge { color: var(--gold); } .mid { min-width: 0; display: flex; flex-direction: column; justify-content: space-between; padding: 1px 0; } .l1 { display: flex; align-items: baseline; gap: 6px; min-width: 0; } .nm { font-weight: 650; font-size: 14px; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 62%; } .sy { color: var(--tx3); font-weight: 500; font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } .cp { color: var(--tx4); align-self: center; opacity: 0; transition: opacity .12s, color .12s; } .row:hover .cp { opacity: 1; } .cp:hover { color: var(--tx); } .l2 { display: flex; align-items: center; gap: 8px; color: var(--tx3); font-size: 12px; height: 18px; } .agev { color: var(--up); font-weight: 600; min-width: 26px; } .agev.old { color: var(--tx2); } .soc { display: flex; align-items: center; gap: 6px; } .soc a { color: var(--tx3); transition: color .12s; display: grid; place-items: center; } .soc a:hover { color: var(--tx); } .soc a.off { color: var(--tx4); opacity: .4; pointer-events: none; } .hc { display: flex; align-items: center; gap: 3px; color: var(--tx2); font-weight: 550; } .l3 { display: flex; gap: 4px; overflow: hidden; height: 20px; } .right { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; min-width: 92px; padding: 1px 0; } .stat { display: flex; align-items: baseline; gap: 5px; white-space: nowrap; } .stat .k { font-size: 11px; color: var(--tx4); font-weight: 600; } .stat .v { font-weight: 650; font-size: 15px; letter-spacing: -.01em; transition: color .5s; } .stat .v.sm { font-size: 12.5px; font-weight: 600; color: var(--tx2); } .v.fl-up { color: var(--up) !important; transition: none; } .v.fl-down { color: var(--down) !important; transition: none; } .txbar { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--tx2); font-weight: 600; } .bar { width: 38px; height: 3px; border-radius: 3px; background: var(--down); overflow: hidden; opacity: .9; } .bar i { display: block; height: 100%; background: var(--up); } .qb { display: inline-flex; align-items: center; gap: 4px; height: 24px; padding: 0 9px 0 7px; border-radius: 12px; background: var(--acc-bg); color: var(--acc); font-weight: 650; font-size: 12px; transition: background .12s, color .12s, transform .08s; } .qb:hover { background: var(--acc); color: #fff; } .qb:active { transform: scale(.96); } .hide-btn { position: absolute; top: 8px; right: 8px; width: 22px; height: 22px; border-radius: 5px; display: grid; place-items: center; color: var(--tx4); background: var(--bg3); opacity: 0; transition: opacity .12s, color .12s; } .row:hover .hide-btn { opacity: 1; } .hide-btn:hover { color: var(--tx); } .row .right { transition: transform .12s; } .row:hover .right { transform: translateX(-26px); } .col-empty { height: 100%; } .pop { position: fixed; z-index: 30; width: 300px; background: var(--bg2); border: 1px solid var(--line2); border-radius: 10px; box-shadow: 0 24px 60px rgba(0, 0, 0, .6); padding: 14px; display: grid; gap: 12px; animation: pop .14s ease-out; } .pop h4 { margin: 0; font-size: 13px; font-weight: 650; display: flex; justify-content: space-between; align-items: center; } .pop .two { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .pop .inp { height: 30px; width: 100%; } .checks { display: flex; flex-wrap: wrap; gap: 6px; } .ck { height: 26px; padding: 0 10px; border-radius: 13px; border: 1px solid var(--line2); color: var(--tx3); font-size: 12px; font-weight: 550; display: inline-flex; align-items: center; gap: 5px; } .ck.on { color: var(--acc); border-color: rgba(77, 141, 255, .4); background: var(--acc-bg); } .filter-on { color: var(--acc); } /* ---------- coin page ---------- */ .coin { display: grid; grid-template-columns: minmax(0, 1fr) 340px; grid-template-rows: auto minmax(0, 1fr); height: 100%; } .band { grid-column: 1 / -1; display: flex; align-items: center; gap: 16px; padding: 12px 18px; border-bottom: 1px solid var(--line); background: var(--bg1); min-height: 74px; } .band .av { width: 48px; height: 48px; border-radius: 8px; } .band-id { min-width: 0; display: grid; gap: 3px; } .band-id .l1 { gap: 8px; align-items: center; } .band-id .nm { font-size: 16px; max-width: 300px; } .band-id .l2 { gap: 10px; } .addr { display: inline-flex; align-items: center; gap: 5px; color: var(--tx3); font-family: var(--fm); font-size: 11.5px; cursor: pointer; } .addr:hover { color: var(--tx); } .hero { margin-left: 18px; padding-left: 18px; border-left: 1px solid var(--line2); display: grid; gap: 1px; } .hero .k { font-size: 11.5px; color: var(--tx3); font-weight: 500; } .hero .v { font-size: 22px; font-weight: 700; letter-spacing: -.025em; } .kv { display: grid; gap: 3px; padding: 0 14px; border-left: 1px solid var(--line); } .kv .k { font-size: 11.5px; color: var(--tx3); font-weight: 500; white-space: nowrap; } .kv .v { font-size: 13.5px; font-weight: 600; white-space: nowrap; } sub.z { font-size: .68em; vertical-align: -.25em; margin: 0 .5px; color: var(--tx2); } .band-stats { display: flex; align-items: center; margin-left: 4px; overflow: hidden; } .band-r { margin-left: auto; display: flex; gap: 8px; } .main { display: grid; grid-template-rows: minmax(260px, 1fr) minmax(220px, 38%); min-width: 0; min-height: 0; border-right: 1px solid var(--line); } .chart-wrap { display: flex; flex-direction: column; min-height: 0; border-bottom: 1px solid var(--line); } .chart-bar { flex: none; height: 38px; display: flex; align-items: center; gap: 4px; padding: 0 10px; border-bottom: 1px solid var(--line); } .seg { display: flex; background: var(--bg2); border: 1px solid var(--line); border-radius: var(--r); padding: 2px; gap: 1px; } .seg button { height: 24px; padding: 0 8px; border-radius: 4px; color: var(--tx3); font-weight: 550; font-size: 12px; transition: color .12s, background .12s; } .seg button:hover { color: var(--tx); } .seg button.on { color: var(--tx); background: var(--bg4); } .ohlc { margin-left: 10px; font-size: 11.5px; color: var(--tx3); display: flex; gap: 10px; white-space: nowrap; overflow: hidden; } .ohlc b { color: var(--tx2); font-weight: 600; } .chart { flex: 1; min-height: 0; position: relative; } .chart-note { position: absolute; inset: 0; display: grid; place-items: center; color: var(--tx3); pointer-events: none; z-index: 2; } .tabs { display: flex; flex-direction: column; min-height: 0; } .tabs-h { flex: none; height: 40px; display: flex; align-items: stretch; gap: 2px; padding: 0 10px; border-bottom: 1px solid var(--line); } .tabs-h button { padding: 0 10px; color: var(--tx3); font-weight: 550; position: relative; display: flex; align-items: center; gap: 6px; } .tabs-h button:hover { color: var(--tx); } .tabs-h button.on { color: var(--tx); } .tabs-h button.on::after { content: ''; position: absolute; left: 8px; right: 8px; bottom: -1px; height: 2px; border-radius: 2px; background: var(--acc); } .tabs-h .count { font-size: 10.5px; } .tab-body { flex: 1; overflow: auto; min-height: 0; } table.t { width: 100%; border-collapse: collapse; font-size: 12.5px; } .t th { position: sticky; top: 0; background: var(--bg); z-index: 1; text-align: left; font-weight: 550; color: var(--tx4); font-size: 11.5px; padding: 8px 12px; border-bottom: 1px solid var(--line); white-space: nowrap; } .t td { padding: 0 12px; height: 34px; border-bottom: 1px solid var(--line); white-space: nowrap; } .t tr:hover td { background: var(--bg1); } .t .r { text-align: right; } .t tr.fresh td { animation: rowIn 1.2s ease-out; } .t tr.me td { background: rgba(77, 141, 255, .04); } .t a.w { color: var(--tx2); font-family: var(--fm); font-size: 11.5px; } .t a.w:hover { color: var(--acc); } .t .ext { color: var(--tx4); display: inline-grid; } .t .ext:hover { color: var(--tx); } .side { overflow-y: auto; background: var(--bg); min-height: 0; } .card { border-bottom: 1px solid var(--line); padding: 14px 16px; display: grid; gap: 12px; } .card-h { display: flex; align-items: center; justify-content: space-between; font-size: 12.5px; font-weight: 600; color: var(--tx2); } .bs { display: grid; grid-template-columns: 1fr 1fr; background: var(--bg2); border: 1px solid var(--line); border-radius: 8px; padding: 3px; } .bs button { height: 32px; border-radius: 6px; font-weight: 650; color: var(--tx3); transition: color .12s, background .12s; } .bs button.on.b { background: var(--up-bg); color: var(--up); } .bs button.on.s { background: var(--down-bg); color: var(--down); } .presets { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; } .presets button { height: 32px; border-radius: 6px; background: var(--bg2); border: 1px solid var(--line2); font-weight: 600; font-size: 12.5px; color: var(--tx2); transition: border-color .12s, color .12s, background .12s; } .presets button:hover { border-color: var(--line3); color: var(--tx); } .presets button.on { border-color: var(--acc2); color: var(--acc); background: var(--acc-bg); } .amt { display: flex; align-items: center; height: 40px; border-radius: 8px; background: var(--bg2); border: 1px solid var(--line2); padding: 0 12px; gap: 8px; transition: border-color .15s; } .amt:focus-within { border-color: var(--acc2); } .amt input { flex: 1; min-width: 0; border: 0; outline: 0; background: none; font-size: 15px; font-weight: 600; } .amt span { color: var(--tx3); font-weight: 600; font-size: 12.5px; } .go { height: 42px; border-radius: 8px; font-weight: 700; font-size: 14px; letter-spacing: -.01em; transition: filter .12s, transform .08s; } .go.b { background: var(--up); color: #032015; } .go.s { background: var(--down); color: #2a0610; } .go:hover { filter: brightness(1.08); } .go:active { transform: scale(.99); } .quote { display: flex; justify-content: space-between; font-size: 12px; color: var(--tx3); } .quote b { color: var(--tx2); font-weight: 600; } .pos { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; } .pos > div { background: var(--bg1); padding: 10px 12px; display: grid; gap: 3px; } .pos .k { font-size: 11.5px; color: var(--tx3); } .pos .v { font-weight: 650; font-size: 14px; } .grid4 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; } .cell { background: var(--bg1); border: 1px solid var(--line); border-radius: 8px; padding: 9px 8px 8px; display: grid; gap: 4px; justify-items: center; text-align: center; } .cell .v { font-weight: 650; font-size: 13.5px; } .cell .k { font-size: 11px; color: var(--tx3); display: flex; align-items: center; gap: 4px; } .cell.lv-g .v { color: var(--up); } .cell.lv-b .v { color: var(--down); } .cell.lv-m .v { color: var(--warn); } .prog { display: grid; gap: 7px; } .prog-h { display: flex; justify-content: space-between; font-size: 12px; color: var(--tx3); } .prog-h b { color: var(--tx); font-weight: 650; } .prog-bar { height: 6px; border-radius: 6px; background: var(--bg3); overflow: hidden; } .prog-bar i { display: block; height: 100%; border-radius: 6px; background: linear-gradient(90deg, var(--acc2), var(--acc)); box-shadow: 0 0 12px rgba(77, 141, 255, .5); transition: width .6s ease; } .desc { color: var(--tx2); font-size: 12.5px; line-height: 1.5; word-break: break-word; } .links { display: flex; gap: 6px; flex-wrap: wrap; } .dev-cards { display: grid; gap: 1px; } .dc { display: grid; grid-template-columns: 36px 1fr auto auto; gap: 12px; align-items: center; padding: 8px 14px; border-bottom: 1px solid var(--line); } .dc:hover { background: var(--bg1); } .dc .av { width: 36px; height: 36px; border-radius: 6px; } .dc .ph { font-size: 13px; } .dc .r { text-align: right; display: grid; gap: 2px; font-size: 12px; } /* ---------- portfolio / trackers ---------- */ .wrap { max-width: 1240px; margin: 0 auto; padding: 24px 24px 60px; display: grid; gap: 20px; } .ph-h { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; } .ph-h h1 { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -.025em; } .ph-h p { margin: 4px 0 0; color: var(--tx3); } .tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; } .tile { background: var(--bg1); border: 1px solid var(--line); border-radius: 10px; padding: 16px 18px; display: grid; gap: 6px; } .tile .k { color: var(--tx3); font-size: 12px; font-weight: 500; } .tile .v { font-size: 22px; font-weight: 700; letter-spacing: -.02em; } .tile .s { color: var(--tx3); font-size: 12px; } .panel { background: var(--bg1); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; } .panel-h { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--line); } .panel-h h3 { margin: 0; font-size: 14px; font-weight: 650; } .panel .t th { background: var(--bg1); } .coin-cell { display: flex; align-items: center; gap: 10px; } .coin-cell .av { width: 30px; height: 30px; border-radius: 6px; } .coin-cell .ph { font-size: 12px; } .coin-cell b { font-weight: 600; } .trk { display: grid; grid-template-columns: 360px minmax(0, 1fr); gap: 16px; align-items: start; } .trk-list { display: grid; } .tw { display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--line); } .tw:hover { background: var(--bg2); } .tw .nmw { font-weight: 600; display: flex; align-items: center; gap: 6px; } .tw .acts { display: flex; gap: 2px; opacity: .55; transition: opacity .12s; } .tw:hover .acts { opacity: 1; } .add-form { display: grid; gap: 8px; padding: 14px; border-bottom: 1px solid var(--line); } .add-form .two { display: grid; grid-template-columns: 1fr 120px; gap: 8px; } .feed-row td { height: 40px; } /* ---------- firehose ---------- */ .live-wrap { max-width: 1400px; height: 100%; grid-template-rows: auto auto minmax(0, 1fr); padding-bottom: 24px; } .tape-panel { display: flex; flex-direction: column; min-height: 0; } .tape-tools { display: flex; align-items: center; gap: 8px; } .tape-scroll { flex: 1; overflow: auto; min-height: 0; } .min-sol { display: flex; align-items: center; gap: 6px; height: 30px; padding: 0 10px; border-radius: var(--r); background: var(--bg2); border: 1px solid var(--line); color: var(--tx3); font-size: 12px; font-weight: 550; } .min-sol input { width: 44px; border: 0; outline: 0; background: none; color: var(--tx); font-weight: 600; } .live-state { font-size: 12px; font-weight: 600; color: var(--warn); } .tape td { height: 36px; } .tape .coin-cell { gap: 8px; } .tape .coin-cell .av { width: 26px; height: 26px; border-radius: 5px; } .tape .coin-cell .ph { font-size: 10px; } .tape-name { font-size: 12px; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tape tr.big td { background: rgba(255, 191, 87, .045); } .tape tr.big td:nth-child(4) { font-weight: 700; } @media (max-width: 1100px) { .pulse { grid-template-columns: 1fr; overflow: auto; } .col { height: 70vh; } .coin { grid-template-columns: 1fr; grid-template-rows: auto; overflow: auto; } .tiles { grid-template-columns: repeat(2, 1fr); } .trk { grid-template-columns: 1fr; } .search { width: 220px; } } ===== FILE: public/fonts/OFL.txt ===== sha256: 930853ee1daa68554d9e35c8a9175affb74f699fad9a5da6ee5ebe76379d9137 · bytes: 4368 · lines: 92 Copyright (c) 2023 Vercel, in collaboration with basement.studio This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION AND CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ===== FILE: public/img/mark.svg ===== sha256: 6e1afb1525f702acd542da43613464686c066471fb24d178414eeda743aac409 · bytes: 762 · lines: 16 ===== FILE: .env.example ===== sha256: 18928b104a3b082710039cdf62637cabf02bad55b5a0b8439ae9986ddfd9494d · bytes: 1004 · lines: 20 # Optional. Copy to .env and change what you need: OScope runs fine without this file. # Port of the page (if 3000 is taken and you did not set PORT, OScope picks the next free one) PORT=3000 # 127.0.0.1 = only this computer. 0.0.0.0 = also other devices on your network. HOST=127.0.0.1 # Live trade stream. Free, no key. Point it at your own firehose to share one connection. FEED_URL=wss://stream.pumpapi.io/ # OScope passes the stream on at ws://localhost:3000/firehose for your own tools. off = turn it off. FIREHOSE=on # Public IPFS gateways asked for coin pictures (comma separated, each ending in /ipfs/) IPFS_GATEWAYS=https://ipfs.filebase.io/ipfs/,https://4everland.io/ipfs/,https://pump.mypinata.cloud/ipfs/ # Where OScope keeps what it saw between runs DATA_DIR=data # Curve progress (%) at which a coin moves to Final Stretch STRETCH_AT=60 # How long a coin stays in New Pairs (minutes) NEW_MAX_AGE_MIN=60 # A line in the terminal every N seconds with the trade count (0 = off) LOG_EVERY=15 ===== FILE: .gitignore ===== sha256: c8406607266ae22fd29116a2df22231b723b39fe2c8faedabfd83352e98dad9a · bytes: 62 · lines: 7 node_modules/ data/ .env .env.* !.env.example *.log .DS_Store ===== FILE: LICENSE ===== sha256: 30c4934f73fd23f059d3dfba8957a21877e9cdd8cc572d53bb64cf4b9441f135 · bytes: 1076 · lines: 21 MIT License Copyright (c) 2026 OScope contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ===== END OF TEXT FILES ===== Binary files (pictures and fonts, not shown; check them against the manifest): 3cda0d770afbae013cfe9b7ec273e010aaebac0321192197879aa9f9b23a1092 docs/media/coin.jpg (179836 bytes) 8b6d72ab72ded05add3292e73ef283bf20b2ab5f454679b8b1ef477562b129dd docs/media/logo.png (69106 bytes) ff1f38a74800b6f88cc4b2ee4b4c78167d8f62b7da9897ed9adb89f93f78bc1e docs/media/pulse.jpg (309021 bytes) bbce1824c8e263cd7e212dde15525790026440d1c10e60733de9f82958bdada4 docs/media/trailer.webp (3258392 bytes) a369fcf5628ea2aa4e1b9e2ec6a5b3624e365bda588e1f0f2f12b564f728fbb8 public/fonts/Geist-Variable.woff2 (69652 bytes) fba8f577f38a2bbcbe818efa6348dd58f36303a10b8737c42fefad275be563ab public/fonts/GeistMono-Variable.woff2 (71368 bytes)