forked from winnie/MinePanel
584 lines
20 KiB
HTML
584 lines
20 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>MinePanel Dashboard - Themes</title>
|
|
<link rel="stylesheet" href="/panel.css">
|
|
<script src="/theme.js"></script>
|
|
</head>
|
|
<body>
|
|
<div class="layout">
|
|
<aside class="sidebar">
|
|
<div class="sidebar-brand">MinePanel</div>
|
|
<div id="me" class="sidebar-meta"></div>
|
|
<nav class="side-nav">
|
|
<a class="side-link" href="/dashboard/overview">Overview</a>
|
|
<button class="side-category-toggle" data-category="server" type="button">Server</button>
|
|
<div class="side-category-items" data-category-items="server">
|
|
<a class="side-link" href="/console">Console</a>
|
|
<a class="side-link" href="/dashboard/resources">Resources</a>
|
|
<a class="side-link" href="/dashboard/players">Players</a>
|
|
<a class="side-link" href="/dashboard/bans">Bans</a>
|
|
<a class="side-link" href="/dashboard/plugins">Plugins</a>
|
|
</div>
|
|
<button class="side-category-toggle" data-category="panel" type="button">Panel</button>
|
|
<div class="side-category-items" data-category-items="panel">
|
|
<a class="side-link" href="/dashboard/users">Users</a>
|
|
<a class="side-link" href="/dashboard/discord-webhook">Discord Webhook</a>
|
|
<a class="side-link active" href="/dashboard/themes">Themes</a>
|
|
</div>
|
|
</nav>
|
|
<button id="logout" class="secondary sidebar-logout">Logout</button>
|
|
</aside>
|
|
|
|
<main class="content-area">
|
|
<h1>Themes</h1>
|
|
<p>Pick a preset or customize panel colors.</p>
|
|
|
|
<section class="card spaced-top">
|
|
<h2>Presets</h2>
|
|
<div class="theme-preset-row">
|
|
<select id="themePreset">
|
|
<option value="default">Default (MinePanel)</option>
|
|
<option value="midnight">Midnight Blue</option>
|
|
<option value="forest">Forest Dark</option>
|
|
<option value="ember">Ember Dark</option>
|
|
</select>
|
|
<button id="applyPreset" type="button">Apply Preset</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card spaced-top">
|
|
<h2>Custom Colors</h2>
|
|
<div id="themeGrid" class="theme-grid"></div>
|
|
<div class="theme-actions">
|
|
<button id="saveTheme" type="button">Save Custom Theme</button>
|
|
<button id="resetTheme" class="secondary" type="button">Reset to Default</button>
|
|
</div>
|
|
<p id="themeStatus" class="action-status"></p>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
function initSidebarCategories() {
|
|
const storageKey = 'minepanel.sidebar.categories';
|
|
let savedState = {};
|
|
try {
|
|
savedState = JSON.parse(sessionStorage.getItem(storageKey) || '{}') || {};
|
|
} catch (ignored) {
|
|
savedState = {};
|
|
}
|
|
|
|
function persistState() {
|
|
try {
|
|
sessionStorage.setItem(storageKey, JSON.stringify(savedState));
|
|
} catch (ignored) {
|
|
// Ignore unavailable sessionStorage.
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll('.side-category-toggle').forEach(toggle => {
|
|
const category = toggle.dataset.category;
|
|
const items = document.querySelector(`.side-category-items[data-category-items="${category}"]`);
|
|
if (!items || !category) return;
|
|
|
|
const expanded = savedState[category] === true;
|
|
items.classList.toggle('expanded', expanded);
|
|
toggle.classList.toggle('expanded', expanded);
|
|
|
|
toggle.addEventListener('click', () => {
|
|
const expanded = !items.classList.contains('expanded');
|
|
items.classList.toggle('expanded', expanded);
|
|
toggle.classList.toggle('expanded', expanded);
|
|
savedState[category] = expanded;
|
|
persistState();
|
|
});
|
|
});
|
|
}
|
|
|
|
const STORAGE_KEY = window.MinePanelTheme ? window.MinePanelTheme.storageKey : 'minepanel.customTheme';
|
|
const THEME_KEYS = [
|
|
'--bg',
|
|
'--surface',
|
|
'--surface-2',
|
|
'--border',
|
|
'--text',
|
|
'--text-muted',
|
|
'--accent',
|
|
'--accent-strong',
|
|
'--danger-bg',
|
|
'--danger-text',
|
|
'--log-bg',
|
|
'--sidebar-bg',
|
|
'--sidebar-border',
|
|
'--sidebar-meta-bg',
|
|
'--sidebar-meta-border',
|
|
'--sidebar-toggle-text',
|
|
'--sidebar-toggle-icon',
|
|
'--sidebar-link-text',
|
|
'--sidebar-link-bg',
|
|
'--sidebar-link-border',
|
|
'--sidebar-link-hover-bg',
|
|
'--button-bg',
|
|
'--button-border',
|
|
'--button-hover-bg',
|
|
'--button-secondary-bg',
|
|
'--button-secondary-border',
|
|
'--input-bg',
|
|
'--focus-ring',
|
|
'--table-bg',
|
|
'--table-head-bg',
|
|
'--table-row-border',
|
|
'--table-row-hover-bg',
|
|
'--table-head-text',
|
|
'--table-cell-text',
|
|
'--log-border',
|
|
'--log-text',
|
|
'--chart-bg',
|
|
'--chart-border',
|
|
'--chart-grid',
|
|
'--chart-label',
|
|
'--player-box-bg',
|
|
'--player-box-border',
|
|
'--player-box-hover-bg',
|
|
'--player-box-active-bg',
|
|
'--player-box-active-border',
|
|
'--player-muted-bg',
|
|
'--player-muted-border'
|
|
];
|
|
|
|
const THEME_GROUPS = [
|
|
{
|
|
title: 'Core',
|
|
keys: [
|
|
'--bg',
|
|
'--surface',
|
|
'--surface-2',
|
|
'--border',
|
|
'--text',
|
|
'--text-muted',
|
|
'--accent',
|
|
'--accent-strong',
|
|
'--danger-bg',
|
|
'--danger-text'
|
|
]
|
|
},
|
|
{
|
|
title: 'Sidebar',
|
|
keys: [
|
|
'--sidebar-bg',
|
|
'--sidebar-border',
|
|
'--sidebar-meta-bg',
|
|
'--sidebar-meta-border',
|
|
'--sidebar-toggle-text',
|
|
'--sidebar-toggle-icon',
|
|
'--sidebar-link-text',
|
|
'--sidebar-link-bg',
|
|
'--sidebar-link-border',
|
|
'--sidebar-link-hover-bg'
|
|
]
|
|
},
|
|
{
|
|
title: 'Buttons and Inputs',
|
|
keys: [
|
|
'--button-bg',
|
|
'--button-border',
|
|
'--button-hover-bg',
|
|
'--button-secondary-bg',
|
|
'--button-secondary-border',
|
|
'--input-bg',
|
|
'--focus-ring'
|
|
]
|
|
},
|
|
{
|
|
title: 'Logs and Tables',
|
|
keys: [
|
|
'--log-bg',
|
|
'--log-border',
|
|
'--log-text',
|
|
'--table-bg',
|
|
'--table-head-bg',
|
|
'--table-row-border',
|
|
'--table-row-hover-bg',
|
|
'--table-head-text',
|
|
'--table-cell-text'
|
|
]
|
|
},
|
|
{
|
|
title: 'Charts',
|
|
keys: [
|
|
'--chart-bg',
|
|
'--chart-border',
|
|
'--chart-grid',
|
|
'--chart-label'
|
|
]
|
|
},
|
|
{
|
|
title: 'Players',
|
|
keys: [
|
|
'--player-box-bg',
|
|
'--player-box-border',
|
|
'--player-box-hover-bg',
|
|
'--player-box-active-bg',
|
|
'--player-box-active-border',
|
|
'--player-muted-bg',
|
|
'--player-muted-border'
|
|
]
|
|
}
|
|
];
|
|
|
|
const PRESETS = {
|
|
default: {
|
|
'--bg': '#0b1220',
|
|
'--surface': '#111a2e',
|
|
'--surface-2': '#16233d',
|
|
'--border': '#24324f',
|
|
'--text': '#e5ebf8',
|
|
'--text-muted': '#99a8c6',
|
|
'--accent': '#4f8cff',
|
|
'--accent-strong': '#3a75e8',
|
|
'--danger-bg': '#3d1d29',
|
|
'--danger-text': '#ffb7c7',
|
|
'--log-bg': '#0a101d',
|
|
'--sidebar-bg': '#060b16',
|
|
'--sidebar-border': '#1b2942',
|
|
'--sidebar-meta-bg': '#0d1528',
|
|
'--sidebar-meta-border': '#1f3050',
|
|
'--sidebar-toggle-text': '#7f93b8',
|
|
'--sidebar-toggle-icon': '#8fa6d0',
|
|
'--sidebar-link-text': '#c7d7f7',
|
|
'--sidebar-link-bg': '#0f1930',
|
|
'--sidebar-link-border': '#263a5d',
|
|
'--sidebar-link-hover-bg': '#162642',
|
|
'--button-bg': '#4f8cff',
|
|
'--button-border': '#3a75e8',
|
|
'--button-hover-bg': '#5a95ff',
|
|
'--button-secondary-bg': '#21314d',
|
|
'--button-secondary-border': '#344769',
|
|
'--input-bg': '#0f1930',
|
|
'--focus-ring': '#4f8cff',
|
|
'--table-bg': '#0e172b',
|
|
'--table-head-bg': '#13203b',
|
|
'--table-row-border': '#1f2d49',
|
|
'--table-row-hover-bg': '#111d35',
|
|
'--table-head-text': '#d7e2f8',
|
|
'--table-cell-text': '#c7d4ee',
|
|
'--log-border': '#213457',
|
|
'--log-text': '#d6e0f8',
|
|
'--chart-bg': '#0b1427',
|
|
'--chart-border': '#213457',
|
|
'--chart-grid': '#7896d2',
|
|
'--chart-label': '#8ea6d2',
|
|
'--player-box-bg': '#0f1930',
|
|
'--player-box-border': '#23365a',
|
|
'--player-box-hover-bg': '#162642',
|
|
'--player-box-active-bg': '#1b2c4a',
|
|
'--player-box-active-border': '#3f73cf',
|
|
'--player-muted-bg': '#101a2f',
|
|
'--player-muted-border': '#2a3f63'
|
|
},
|
|
midnight: {
|
|
'--bg': '#070b17',
|
|
'--surface': '#0f1a30',
|
|
'--surface-2': '#132445',
|
|
'--border': '#263f67',
|
|
'--text': '#e6f0ff',
|
|
'--text-muted': '#97abd2',
|
|
'--accent': '#4f8cff',
|
|
'--accent-strong': '#2c6fe2',
|
|
'--danger-bg': '#3b1a2f',
|
|
'--danger-text': '#ffb5c6',
|
|
'--log-bg': '#090f1d',
|
|
'--sidebar-bg': '#050915',
|
|
'--sidebar-border': '#1b3158',
|
|
'--sidebar-meta-bg': '#0c1730',
|
|
'--sidebar-meta-border': '#1f3a67',
|
|
'--sidebar-toggle-text': '#8aa1ca',
|
|
'--sidebar-toggle-icon': '#9cb4e2',
|
|
'--sidebar-link-text': '#d2e2ff',
|
|
'--sidebar-link-bg': '#11203b',
|
|
'--sidebar-link-border': '#2b4773',
|
|
'--sidebar-link-hover-bg': '#183055',
|
|
'--button-bg': '#4f8cff',
|
|
'--button-border': '#2c6fe2',
|
|
'--button-hover-bg': '#5f98ff',
|
|
'--button-secondary-bg': '#23385e',
|
|
'--button-secondary-border': '#35507e',
|
|
'--input-bg': '#11203b',
|
|
'--focus-ring': '#4f8cff',
|
|
'--table-bg': '#0f1a31',
|
|
'--table-head-bg': '#13274a',
|
|
'--table-row-border': '#2a4773',
|
|
'--table-row-hover-bg': '#183157',
|
|
'--table-head-text': '#d7e6ff',
|
|
'--table-cell-text': '#cddcff',
|
|
'--log-border': '#2b4a78',
|
|
'--log-text': '#d4e3ff',
|
|
'--chart-bg': '#0f1d38',
|
|
'--chart-border': '#2b4a78',
|
|
'--chart-grid': '#87a7df',
|
|
'--chart-label': '#a3b8df',
|
|
'--player-box-bg': '#11203b',
|
|
'--player-box-border': '#2b4773',
|
|
'--player-box-hover-bg': '#183055',
|
|
'--player-box-active-bg': '#223c69',
|
|
'--player-box-active-border': '#4b82de',
|
|
'--player-muted-bg': '#0f1b34',
|
|
'--player-muted-border': '#2a4268'
|
|
},
|
|
forest: {
|
|
'--bg': '#0b1612',
|
|
'--surface': '#11231b',
|
|
'--surface-2': '#173026',
|
|
'--border': '#29503f',
|
|
'--text': '#e7fff5',
|
|
'--text-muted': '#97c7b0',
|
|
'--accent': '#3fbf7f',
|
|
'--accent-strong': '#2ca866',
|
|
'--danger-bg': '#3d1d29',
|
|
'--danger-text': '#ffb7c7',
|
|
'--log-bg': '#0b1511',
|
|
'--sidebar-bg': '#08130f',
|
|
'--sidebar-border': '#1f3f33',
|
|
'--sidebar-meta-bg': '#10231c',
|
|
'--sidebar-meta-border': '#2a4f40',
|
|
'--sidebar-toggle-text': '#8db8a3',
|
|
'--sidebar-toggle-icon': '#9fceb8',
|
|
'--sidebar-link-text': '#d6f5e7',
|
|
'--sidebar-link-bg': '#133026',
|
|
'--sidebar-link-border': '#2f5f4b',
|
|
'--sidebar-link-hover-bg': '#1a3d31',
|
|
'--button-bg': '#3fbf7f',
|
|
'--button-border': '#2ca866',
|
|
'--button-hover-bg': '#4dce8d',
|
|
'--button-secondary-bg': '#24493b',
|
|
'--button-secondary-border': '#356652',
|
|
'--input-bg': '#133026',
|
|
'--focus-ring': '#3fbf7f',
|
|
'--table-bg': '#11271f',
|
|
'--table-head-bg': '#173629',
|
|
'--table-row-border': '#2e5b48',
|
|
'--table-row-hover-bg': '#1a3f31',
|
|
'--table-head-text': '#daf5e8',
|
|
'--table-cell-text': '#c8ebda',
|
|
'--log-border': '#2e5a49',
|
|
'--log-text': '#d9f4e8',
|
|
'--chart-bg': '#10261e',
|
|
'--chart-border': '#2e5a49',
|
|
'--chart-grid': '#7fb6a1',
|
|
'--chart-label': '#94c7b3',
|
|
'--player-box-bg': '#133026',
|
|
'--player-box-border': '#2f5f4b',
|
|
'--player-box-hover-bg': '#1a3d31',
|
|
'--player-box-active-bg': '#23503f',
|
|
'--player-box-active-border': '#43a379',
|
|
'--player-muted-bg': '#11271f',
|
|
'--player-muted-border': '#2c5342'
|
|
},
|
|
ember: {
|
|
'--bg': '#18100b',
|
|
'--surface': '#2a1a11',
|
|
'--surface-2': '#3a2416',
|
|
'--border': '#5a3a23',
|
|
'--text': '#fff0e8',
|
|
'--text-muted': '#d6b8a4',
|
|
'--accent': '#ff8f4f',
|
|
'--accent-strong': '#e67839',
|
|
'--danger-bg': '#4a1f22',
|
|
'--danger-text': '#ffb7b7',
|
|
'--log-bg': '#1a120d',
|
|
'--sidebar-bg': '#140d08',
|
|
'--sidebar-border': '#4c311f',
|
|
'--sidebar-meta-bg': '#23160e',
|
|
'--sidebar-meta-border': '#5e3b26',
|
|
'--sidebar-toggle-text': '#d1a78f',
|
|
'--sidebar-toggle-icon': '#e0b69e',
|
|
'--sidebar-link-text': '#ffe4d2',
|
|
'--sidebar-link-bg': '#382116',
|
|
'--sidebar-link-border': '#6f4228',
|
|
'--sidebar-link-hover-bg': '#4a2c1d',
|
|
'--button-bg': '#ff8f4f',
|
|
'--button-border': '#e67839',
|
|
'--button-hover-bg': '#ffa366',
|
|
'--button-secondary-bg': '#5a3622',
|
|
'--button-secondary-border': '#7a4a30',
|
|
'--input-bg': '#382116',
|
|
'--focus-ring': '#ff8f4f',
|
|
'--table-bg': '#2f1d13',
|
|
'--table-head-bg': '#432719',
|
|
'--table-row-border': '#6b4027',
|
|
'--table-row-hover-bg': '#4b2e1f',
|
|
'--table-head-text': '#ffe7da',
|
|
'--table-cell-text': '#f2d2bf',
|
|
'--log-border': '#70442a',
|
|
'--log-text': '#ffe4d5',
|
|
'--chart-bg': '#341f14',
|
|
'--chart-border': '#70442a',
|
|
'--chart-grid': '#c18e71',
|
|
'--chart-label': '#d2a98f',
|
|
'--player-box-bg': '#382116',
|
|
'--player-box-border': '#6f4228',
|
|
'--player-box-hover-bg': '#4a2c1d',
|
|
'--player-box-active-bg': '#603924',
|
|
'--player-box-active-border': '#ff9a63',
|
|
'--player-muted-bg': '#311d12',
|
|
'--player-muted-border': '#5b3521'
|
|
}
|
|
};
|
|
|
|
function setStatus(message, isError) {
|
|
const status = document.getElementById('themeStatus');
|
|
status.textContent = message || '';
|
|
status.className = isError ? 'action-status error-text' : 'action-status success-text';
|
|
}
|
|
|
|
function currentValueForKey(key) {
|
|
const value = getComputedStyle(document.documentElement).getPropertyValue(key).trim();
|
|
return value || PRESETS.default[key] || '#000000';
|
|
}
|
|
|
|
function normalizeHex(value) {
|
|
if (!value) return '#000000';
|
|
const trimmed = value.trim();
|
|
if (!trimmed.startsWith('#')) return '#000000';
|
|
if (trimmed.length === 7) return trimmed;
|
|
if (trimmed.length === 4) {
|
|
return `#${trimmed[1]}${trimmed[1]}${trimmed[2]}${trimmed[2]}${trimmed[3]}${trimmed[3]}`;
|
|
}
|
|
return '#000000';
|
|
}
|
|
|
|
function buildThemeField(key) {
|
|
const row = document.createElement('label');
|
|
row.className = 'theme-field';
|
|
row.setAttribute('for', `theme-${key.replace(/[^a-z0-9]/gi, '')}`);
|
|
|
|
const name = document.createElement('span');
|
|
name.textContent = key;
|
|
|
|
const input = document.createElement('input');
|
|
input.type = 'color';
|
|
input.id = `theme-${key.replace(/[^a-z0-9]/gi, '')}`;
|
|
input.dataset.themeKey = key;
|
|
input.value = normalizeHex(currentValueForKey(key));
|
|
input.addEventListener('input', () => {
|
|
document.documentElement.style.setProperty(key, input.value);
|
|
});
|
|
|
|
row.appendChild(name);
|
|
row.appendChild(input);
|
|
return row;
|
|
}
|
|
|
|
function buildThemeGroup(title, keys) {
|
|
const section = document.createElement('section');
|
|
section.className = 'theme-group';
|
|
|
|
const heading = document.createElement('h3');
|
|
heading.className = 'theme-group-title';
|
|
heading.textContent = title;
|
|
|
|
const content = document.createElement('div');
|
|
content.className = 'theme-group-grid';
|
|
|
|
keys.forEach(key => {
|
|
content.appendChild(buildThemeField(key));
|
|
});
|
|
|
|
section.appendChild(heading);
|
|
section.appendChild(content);
|
|
return section;
|
|
}
|
|
|
|
function buildThemeGrid() {
|
|
const grid = document.getElementById('themeGrid');
|
|
grid.innerHTML = '';
|
|
|
|
const usedKeys = new Set();
|
|
THEME_GROUPS.forEach(group => {
|
|
const validKeys = group.keys.filter(key => THEME_KEYS.includes(key));
|
|
if (validKeys.length === 0) {
|
|
return;
|
|
}
|
|
|
|
validKeys.forEach(key => usedKeys.add(key));
|
|
grid.appendChild(buildThemeGroup(group.title, validKeys));
|
|
});
|
|
|
|
const ungrouped = THEME_KEYS.filter(key => !usedKeys.has(key));
|
|
if (ungrouped.length > 0) {
|
|
grid.appendChild(buildThemeGroup('Other', ungrouped));
|
|
}
|
|
}
|
|
|
|
function collectThemeFromInputs() {
|
|
const theme = {};
|
|
document.querySelectorAll('input[data-theme-key]').forEach(input => {
|
|
theme[input.dataset.themeKey] = input.value;
|
|
});
|
|
return theme;
|
|
}
|
|
|
|
function applyTheme(theme) {
|
|
if (window.MinePanelTheme) {
|
|
window.MinePanelTheme.applyTheme(theme);
|
|
} else {
|
|
Object.entries(theme).forEach(([key, value]) => {
|
|
document.documentElement.style.setProperty(key, value);
|
|
});
|
|
}
|
|
buildThemeGrid();
|
|
}
|
|
|
|
async function api(url, options) {
|
|
const res = await fetch(url, options);
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Request failed');
|
|
return data;
|
|
}
|
|
|
|
async function loadMe() {
|
|
const data = await api('/api/me');
|
|
document.getElementById('me').textContent = `Logged in as ${data.user.username} (${data.user.role})`;
|
|
}
|
|
|
|
document.getElementById('applyPreset').addEventListener('click', () => {
|
|
const preset = document.getElementById('themePreset').value;
|
|
const theme = PRESETS[preset] || PRESETS.default;
|
|
applyTheme(theme);
|
|
setStatus(`Applied ${preset} preset. Save to keep it.`, false);
|
|
});
|
|
|
|
document.getElementById('saveTheme').addEventListener('click', () => {
|
|
const theme = collectThemeFromInputs();
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(theme));
|
|
setStatus('Custom theme saved.', false);
|
|
});
|
|
|
|
document.getElementById('resetTheme').addEventListener('click', () => {
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
applyTheme(PRESETS.default);
|
|
setStatus('Theme reset to default.', false);
|
|
});
|
|
|
|
document.getElementById('logout').addEventListener('click', async () => {
|
|
await api('/api/logout', { method: 'POST' });
|
|
window.location.href = '/';
|
|
});
|
|
|
|
(async () => {
|
|
try {
|
|
initSidebarCategories();
|
|
await loadMe();
|
|
buildThemeGrid();
|
|
} catch {
|
|
window.location.href = '/';
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|