first commit
This commit is contained in:
commit
74229b4471
4 files changed
+967
No files matched your search
@@ -0,0 +1,4 @@
|
||||
admin.html
|
||||
admin.js
|
||||
API_REFERENCE.md
|
||||
tachyon_merged_dynamic.html
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>tachyon</title>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
<body class="is-loading">
|
||||
|
||||
<div class="loading-screen" aria-live="polite" aria-busy="true">
|
||||
<div class="loading-bar-shell">
|
||||
<div class="loading-bar-row">
|
||||
<span class="loading-label">initializing tachyon</span>
|
||||
<span class="loading-status">please wait</span>
|
||||
</div>
|
||||
<div class="loading-track" aria-hidden="true">
|
||||
<div class="loading-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navigation-bar imgui-window">
|
||||
<div class="imgui-content">
|
||||
<div class="navigation-logo">
|
||||
<img class="navigation-logo" src="./assets/logo.png" />
|
||||
</div>
|
||||
<span class="nav-spacer">tachyon</span>
|
||||
<span class="nav-date">placeholder date</span>
|
||||
<div class="nav-border"></div>
|
||||
<div class="nav-links">
|
||||
<a href="#" class="nav-link">documentation</a>
|
||||
<div class="nav-border"></div>
|
||||
<a href="#" class="nav-link">panel</a>
|
||||
<a href="#" class="nav-link">register</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="sections-container">
|
||||
<div class="left-section">
|
||||
<div class="imgui-window">
|
||||
<div class="imgui-content">
|
||||
<div class="imgui-heading">news</div>
|
||||
<div class="list-box" id="news-list">
|
||||
<!-- populated by fetchNews() — this placeholder shows only if the fetch fails -->
|
||||
<div class="item-card"><p class="item-content">Loading news…</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="imgui-window">
|
||||
<div class="imgui-content">
|
||||
<div class="imgui-heading">services</div>
|
||||
<div class="services-strip">
|
||||
<div class="service-chip"><div class="dot"></div>service one</div>
|
||||
<div class="service-chip"><div class="dot"></div>service two</div>
|
||||
<div class="service-chip"><div class="dot"></div>service three</div>
|
||||
<div class="service-chip"><div class="dot"></div>service four</div>
|
||||
<div class="service-chip"><div class="dot"></div>service five</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="imgui-window">
|
||||
<div class="imgui-content">
|
||||
<div class="imgui-heading">overview</div>
|
||||
<div class="stat-columns">
|
||||
|
||||
<div>
|
||||
<div class="stat-col-title">current users</div>
|
||||
<div id="current-users"><div class="stat-row"><span>Loading…</span><span class="val"></span></div></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="stat-col-title">recent activity</div>
|
||||
<div id="recent-activity"><div class="stat-row"><span>Loading…</span><span class="val"></span></div></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="stat-col-title">general statistics</div>
|
||||
<div id="general-stats">
|
||||
<div class="stat-card"><h4>loading…</h4></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN -->
|
||||
<div class="right-section">
|
||||
<div class="imgui-window">
|
||||
<div class="imgui-content">
|
||||
<div class="imgui-heading">media</div>
|
||||
<div class="media-grid" id="media-grid">
|
||||
<div class="media-item"><p class="media-title">loading media…</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer imgui-window">
|
||||
<div class="imgui-content">
|
||||
<span class="footer-tagline">server powered by a somali guy on a bike</span>
|
||||
<div class="footer-links">
|
||||
<a href="#" class="footer-link">powered by radium technologies</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./main.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,134 @@
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
bootstrapAdmin();
|
||||
});
|
||||
|
||||
const API_BASE = "http://10.0.0.66:7455/api";
|
||||
|
||||
// Small helper: fetch JSON, throw on non-2xx so callers can catch() it.
|
||||
async function getJSON(path) {
|
||||
const res = await fetch(`${API_BASE}${path}`);
|
||||
if (!res.ok) throw new Error(`${path} responded ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function timeAgo(isoString) {
|
||||
const diffMs = Date.now() - new Date(isoString).getTime();
|
||||
const mins = Math.floor(diffMs / 60000);
|
||||
if (mins < 60) return `${mins}m ago`;
|
||||
const hours = Math.floor(mins / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
return `${Math.floor(hours / 24)}d ago`;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ---------- News ----------
|
||||
async function loadNews() {
|
||||
const el = document.getElementById("news-list");
|
||||
try {
|
||||
const posts = await getJSON("/news?limit=10");
|
||||
if (posts.length === 0) {
|
||||
el.innerHTML = `<div class="item-card"><p class="item-content">No news yet.</p></div>`;
|
||||
return;
|
||||
}
|
||||
el.innerHTML = posts.map(post => `
|
||||
<div class="item-card">
|
||||
<p class="item-title">${escapeHtml(post.title)}<span class="item-timestamp">${timeAgo(post.created_at)}</span></p>
|
||||
<p class="item-content">${escapeHtml(post.content)}</p>
|
||||
</div>
|
||||
`).join("");
|
||||
} catch (err) {
|
||||
console.error("Failed to load news:", err);
|
||||
el.innerHTML = `<div class="item-card"><p class="item-content">Couldn't load news right now.</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Media ----------
|
||||
async function loadMedia() {
|
||||
const el = document.getElementById("media-grid");
|
||||
try {
|
||||
const items = await getJSON("/media?limit=8");
|
||||
if (items.length === 0) {
|
||||
el.innerHTML = `<div class="media-item"><p class="media-title">No media yet.</p></div>`;
|
||||
return;
|
||||
}
|
||||
el.innerHTML = items.map(item => `
|
||||
<div class="media-item">
|
||||
<p class="media-title">${escapeHtml(item.title)}</p>
|
||||
${item.media_type === "video"
|
||||
? `<video class="img-embed" src="${escapeHtml(item.url)}" controls style="width:100%;height:auto;background:#000;"></video>`
|
||||
: `<img class="img-embed" src="${escapeHtml(item.url)}" alt="${escapeHtml(item.title)}" style="width:100%;height:auto;object-fit:cover;">`
|
||||
}
|
||||
</div>
|
||||
`).join("");
|
||||
} catch (err) {
|
||||
console.error("Failed to load media:", err);
|
||||
el.innerHTML = `<div class="media-item"><p class="media-title">Couldn't load media right now.</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Overview (current users / recent activity / general stats) ----------
|
||||
async function loadOverview() {
|
||||
const usersEl = document.getElementById("current-users");
|
||||
const activityEl = document.getElementById("recent-activity");
|
||||
const generalEl = document.getElementById("general-stats");
|
||||
|
||||
try {
|
||||
const data = await getJSON("/stats/overview");
|
||||
|
||||
usersEl.innerHTML = data.currentUsers.length
|
||||
? data.currentUsers.map(u => `
|
||||
<div class="stat-row"><span>${escapeHtml(u.username)}</span><span class="val">now</span></div>
|
||||
`).join("")
|
||||
: `<div class="stat-row"><span>No one online</span><span class="val"></span></div>`;
|
||||
|
||||
activityEl.innerHTML = data.recentActivity.length
|
||||
? data.recentActivity.map(a => `
|
||||
<div class="stat-row"><span>${escapeHtml(a.description)}</span><span class="val">${timeAgo(a.created_at)}</span></div>
|
||||
`).join("")
|
||||
: `<div class="stat-row"><span>No recent activity</span><span class="val"></span></div>`;
|
||||
|
||||
const { users, reports, build } = data.general;
|
||||
generalEl.innerHTML = `
|
||||
<div class="stat-card">
|
||||
<h4>Users</h4>
|
||||
<p>Total: ${users.total}<br>Recent: ${users.mostRecent ? escapeHtml(users.mostRecent.username) : "—"}</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>Reports</h4>
|
||||
<p>Named: ${reports.named}<br>Associated: ${reports.associated}<br>Logged: ${reports.logged} time(s)</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>Build</h4>
|
||||
<p>Pipeline: ${build.pipeline}<br>Build: ${build.buildNumber}<br>Version: ${build.version}</p>
|
||||
</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
console.error("Failed to load overview stats:", err);
|
||||
usersEl.innerHTML = `<div class="stat-row"><span>no active users!</span><span class="val"></span></div>`;
|
||||
activityEl.innerHTML = `<div class="stat-row"><span>no activity!</span><span class="val"></span></div>`;
|
||||
generalEl.innerHTML = `<div class="stat-card"><h4>no statistics!</h4></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function loadAll() {
|
||||
loadNews();
|
||||
loadMedia();
|
||||
loadOverview();
|
||||
}
|
||||
|
||||
async function bootstrapAdmin() {
|
||||
try {
|
||||
await loadAll();
|
||||
} finally {
|
||||
document.body.classList.remove('is-loading');
|
||||
}
|
||||
|
||||
setInterval(loadOverview, 30000);
|
||||
}
|
||||
// Keep "current users" / "recent activity" feeling live without a full reload.
|
||||
setInterval(loadOverview, 30000);
|
||||
@@ -0,0 +1,708 @@
|
||||
:root {
|
||||
--accent: rgb(151, 110, 255);
|
||||
--accent-dark: rgba(151, 110, 255, 0.5);
|
||||
|
||||
/* ImGui window chrome, straight from the original snippet */
|
||||
--outer-fill: rgb(30, 30, 30);
|
||||
--inner-fill: rgb(15, 15, 15);
|
||||
--border-black: rgb(0, 0, 0);
|
||||
--border-grey: rgb(46, 46, 46);
|
||||
--outline: rgb(60, 60, 60);
|
||||
|
||||
--page-bg: #17171a;
|
||||
--text: #ffffff;
|
||||
--text-dim: rgb(150, 150, 155);
|
||||
--text-dimmer: rgb(95, 95, 100);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--page-bg);
|
||||
}
|
||||
|
||||
body.is-loading {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--text);
|
||||
/*font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
font-size: 13px;
|
||||
text-shadow: 0 1px 0 rgba(0,0,0,0.8);*/
|
||||
font-family: Tahoma, Geneva, sans-serif;
|
||||
font-size: 13px;
|
||||
text-rendering: optimizeSpeed;
|
||||
-webkit-font-smoothing: none; /* mac webkit only, toggles grayscale vs subpixel */
|
||||
font-smoothing: none;
|
||||
padding-bottom: 96px;
|
||||
}
|
||||
|
||||
a { color: var(--text); text-decoration: none; }
|
||||
|
||||
.loading-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.loading-bar-shell {
|
||||
width: min(760px, calc(100vw - 16px));
|
||||
background: var(--outer-fill);
|
||||
border: 1px solid var(--border-black);
|
||||
box-shadow: 0 0 0 2px var(--border-grey);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.loading-bar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
text-transform: lowercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.loading-label {
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.loading-status {
|
||||
color: var(--text-dimmer);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.loading-track {
|
||||
height: 8px;
|
||||
background: var(--inner-fill);
|
||||
border: 1px solid var(--border-grey);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading-fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, rgba(151, 110, 255, 0.25), var(--accent), rgba(151, 110, 255, 0.25));
|
||||
transform-origin: left center;
|
||||
animation: loading-fill 5s linear forwards;
|
||||
}
|
||||
|
||||
@keyframes loading-fill {
|
||||
from { transform: scaleX(0); }
|
||||
to { transform: scaleX(1); }
|
||||
}
|
||||
|
||||
body:not(.is-loading) .loading-screen {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity 450ms ease, transform 450ms ease;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
body.is-loading .page-container {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body:not(.is-loading) .page-container {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-thumb { background: var(--accent); border: 1px solid black; }
|
||||
::-webkit-scrollbar-track { background: black; }
|
||||
|
||||
/* ============================================================
|
||||
ImGui window chrome — the shared unit every section is built from.
|
||||
outer fill (30,30,30) -> 1px top accent line -> inner fill (15,15,15)
|
||||
with its own top accent line -> double border (black, then grey)
|
||||
============================================================ */
|
||||
.imgui-window {
|
||||
position: relative;
|
||||
background: var(--outer-fill);
|
||||
border: 1px solid var(--border-black);
|
||||
box-shadow: 0 0 0 2px var(--border-grey);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.imgui-window::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px; left: 2px; right: 2px;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, var(--accent), transparent);
|
||||
}
|
||||
|
||||
.imgui-content {
|
||||
position: relative;
|
||||
background: var(--inner-fill);
|
||||
border: 1px solid var(--border-grey);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.imgui-content::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0; left: 2px; right: 2px;
|
||||
height: 1px;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.imgui-heading {
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: lowercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 2px 0 8px 2px;
|
||||
}
|
||||
|
||||
/* ============ NAV ============ */
|
||||
.navigation-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
margin: 8px 8px 0 8px;
|
||||
}
|
||||
|
||||
.navigation-bar .imgui-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.navigation-logo {
|
||||
width: 16px; height: 16px;
|
||||
margin-right: 8px;
|
||||
/* background: var(--accent); */
|
||||
/* box-shadow: 0 0 8px 1px rgba(127,72,163,0.5); */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-spacer {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.nav-date {
|
||||
color: var(--text-dimmer);
|
||||
font-size: 11.5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.nav-border {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: var(--outline);
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-size: 12.5px;
|
||||
color: var(--text-dim);
|
||||
transition: color .1s ease;
|
||||
}
|
||||
.nav-link:hover { color: var(--accent); }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.nav-links { display: none; }
|
||||
}
|
||||
|
||||
/* ============ CONTAINER / LAYOUT ============ */
|
||||
.page-container {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.sections-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.sections-container { flex-direction: column; }
|
||||
}
|
||||
|
||||
.left-section, .right-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.left-section { flex: 1 1 55%; min-width: 320px; }
|
||||
.right-section { flex: 1 1 38%; min-width: 260px; }
|
||||
|
||||
/* ============ LIST BOX (news / stats scroll areas) ============ */
|
||||
.list-box {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
position: relative;
|
||||
background: var(--outer-fill);
|
||||
border: 1px solid var(--border-black);
|
||||
box-shadow: 0 0 0 1px var(--border-grey);
|
||||
padding: 8px 9px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.item-card:last-child { margin-bottom: 0; }
|
||||
|
||||
.item-title {
|
||||
font-weight: 600;
|
||||
font-size: 12.5px;
|
||||
margin: 0 0 3px 0;
|
||||
}
|
||||
|
||||
.item-timestamp {
|
||||
float: right;
|
||||
color: var(--text-dimmer);
|
||||
font-size: 10.5px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ============ SERVICES STRIP ============ */
|
||||
.services-strip {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.service-chip {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
background: var(--outer-fill);
|
||||
border: 1px solid var(--border-black);
|
||||
box-shadow: 0 0 0 1px var(--border-grey);
|
||||
padding: 6px 11px;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.service-chip .dot {
|
||||
width: 5px; height: 5px;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 6px 0 rgba(127,72,163,0.6);
|
||||
}
|
||||
|
||||
/* ============ STATS GRID ============ */
|
||||
.stat-columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.stat-columns { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.stat-col-title {
|
||||
font-size: 10.5px;
|
||||
text-transform: lowercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-dimmer);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11.5px;
|
||||
padding: 4px 6px;
|
||||
background: var(--outer-fill);
|
||||
border: 1px solid var(--border-black);
|
||||
box-shadow: 0 0 0 1px var(--border-grey);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.stat-row:last-child { margin-bottom: 0; }
|
||||
.stat-row .val { color: var(--text-dim); }
|
||||
|
||||
.stat-card {
|
||||
background: var(--outer-fill);
|
||||
border: 1px solid var(--border-black);
|
||||
box-shadow: 0 0 0 1px var(--border-grey);
|
||||
padding: 7px 9px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.stat-card:last-child { margin-bottom: 0; }
|
||||
|
||||
.stat-card h4 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.stat-card p {
|
||||
margin: 0;
|
||||
font-size: 10.5px;
|
||||
color: var(--text-dim);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ============ MEDIA GRID ============ */
|
||||
.media-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.media-item {
|
||||
background: var(--outer-fill);
|
||||
border: 1px solid var(--border-black);
|
||||
box-shadow: 0 0 0 1px var(--border-grey);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.media-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 5px 2px;
|
||||
}
|
||||
|
||||
.img-embed {
|
||||
position: relative;
|
||||
aspect-ratio: 16/9;
|
||||
background: repeating-linear-gradient(45deg, #101012 0, #101012 8px, #131316 8px, #131316 16px);
|
||||
border: 1px solid var(--border-grey);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-dimmer);
|
||||
font-size: 10.5px;
|
||||
}
|
||||
|
||||
/* ============ FOOTER ============ */
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
margin: 0;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.footer .imgui-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 0;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.footer-tagline {
|
||||
font-size: 11.5px;
|
||||
color: var(--text-dimmer);
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
transition: color .1s ease;
|
||||
}
|
||||
.footer-link:hover { color: var(--accent); }
|
||||
|
||||
/* ============ ADMIN PANEL ============ */
|
||||
.admin-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(300px, 0.85fr);
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.admin-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.panel-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.panel-note {
|
||||
color: var(--text-dimmer);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--border-black);
|
||||
box-shadow: 0 0 0 1px var(--border-grey);
|
||||
background: var(--outer-fill);
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-pill .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 6px rgba(151, 110, 255, 0.55);
|
||||
}
|
||||
|
||||
.admin-form {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-form .field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.admin-form .field-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-size: 10.5px;
|
||||
color: var(--text-dimmer);
|
||||
text-transform: lowercase;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field select,
|
||||
.field textarea {
|
||||
width: 100%;
|
||||
color: var(--text);
|
||||
background: var(--outer-fill);
|
||||
border: 1px solid var(--border-black);
|
||||
box-shadow: 0 0 0 1px var(--border-grey);
|
||||
padding: 8px 9px;
|
||||
font: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.field textarea {
|
||||
min-height: 92px;
|
||||
}
|
||||
|
||||
.field input:focus,
|
||||
.field select:focus,
|
||||
.field textarea:focus,
|
||||
.action-button:focus {
|
||||
outline: 1px solid var(--accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
appearance: none;
|
||||
border: 1px solid var(--border-black);
|
||||
box-shadow: 0 0 0 1px var(--border-grey);
|
||||
background: linear-gradient(to bottom, #2a2a2a, #1b1b1b);
|
||||
color: var(--text);
|
||||
padding: 8px 11px;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.action-button.is-danger {
|
||||
color: #ffb6b6;
|
||||
}
|
||||
|
||||
.action-button.is-danger:hover {
|
||||
color: #ff8080;
|
||||
}
|
||||
|
||||
.action-button.is-inline {
|
||||
padding: 5px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.action-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
background: var(--outer-fill);
|
||||
border: 1px solid var(--border-black);
|
||||
box-shadow: 0 0 0 1px var(--border-grey);
|
||||
padding: 8px 9px;
|
||||
}
|
||||
|
||||
.record-topline {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.record-title {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.record-meta {
|
||||
margin: 0;
|
||||
color: var(--text-dimmer);
|
||||
font-size: 10.5px;
|
||||
}
|
||||
|
||||
.record-body {
|
||||
margin: 0;
|
||||
color: var(--text-dim);
|
||||
font-size: 11.5px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.admin-toast {
|
||||
font-size: 11px;
|
||||
color: var(--text-dimmer);
|
||||
min-height: 1.3em;
|
||||
}
|
||||
|
||||
.admin-toast.is-error {
|
||||
color: #ff8d8d;
|
||||
}
|
||||
|
||||
.admin-toast.is-success {
|
||||
color: #8bd18b;
|
||||
}
|
||||
|
||||
.mini-columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.mini-columns {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.mini-card {
|
||||
background: var(--outer-fill);
|
||||
border: 1px solid var(--border-black);
|
||||
box-shadow: 0 0 0 1px var(--border-grey);
|
||||
padding: 8px 9px;
|
||||
}
|
||||
|
||||
.mini-card h4 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.mini-card p {
|
||||
margin: 0;
|
||||
font-size: 10.5px;
|
||||
color: var(--text-dim);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.endpoint-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.endpoint-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--outer-fill);
|
||||
border: 1px solid var(--border-black);
|
||||
box-shadow: 0 0 0 1px var(--border-grey);
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.endpoint-item .route {
|
||||
color: var(--accent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.endpoint-item .desc {
|
||||
color: var(--text-dimmer);
|
||||
text-align: right;
|
||||
}
|
||||
Reference in new issue
Block a user