mirror of
https://github.com/yggdrasil-network/yggdrasil-go.git
synced 2025-08-24 16:05:07 +03:00
Refactor static file serving in WebUI to allow CSS and JS access without authentication, and implement theme toggle functionality in login and main pages
This commit is contained in:
parent
fc354865ea
commit
3187114780
7 changed files with 408 additions and 126 deletions
|
@ -15,11 +15,20 @@ import (
|
|||
|
||||
// setupStaticHandler configures static file serving for development (files from disk)
|
||||
func setupStaticHandler(mux *http.ServeMux, server *WebUIServer) {
|
||||
// Serve static files from disk for development - with auth
|
||||
// Serve static files from disk for development
|
||||
staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("src/webui/static/")))
|
||||
mux.HandleFunc("/static/", server.authMiddleware(func(rw http.ResponseWriter, r *http.Request) {
|
||||
staticHandler.ServeHTTP(rw, r)
|
||||
}))
|
||||
mux.HandleFunc("/static/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Allow access to CSS and JS files without auth (needed for login page)
|
||||
path := strings.TrimPrefix(r.URL.Path, "/static/")
|
||||
if strings.HasSuffix(path, ".css") || strings.HasSuffix(path, ".js") {
|
||||
staticHandler.ServeHTTP(rw, r)
|
||||
return
|
||||
}
|
||||
// For other static files, require auth
|
||||
server.authMiddleware(func(rw http.ResponseWriter, r *http.Request) {
|
||||
staticHandler.ServeHTTP(rw, r)
|
||||
})(rw, r)
|
||||
})
|
||||
}
|
||||
|
||||
// serveFile serves any file from disk or returns 404 if not found
|
||||
|
|
|
@ -25,12 +25,22 @@ func setupStaticHandler(mux *http.ServeMux, server *WebUIServer) {
|
|||
panic("failed to get embedded static files: " + err.Error())
|
||||
}
|
||||
|
||||
// Serve static files from embedded FS - with auth
|
||||
// Serve static files from embedded FS
|
||||
staticHandler := http.FileServer(http.FS(staticFS))
|
||||
mux.HandleFunc("/static/", server.authMiddleware(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Strip the /static/ prefix before serving
|
||||
http.StripPrefix("/static/", staticHandler).ServeHTTP(rw, r)
|
||||
}))
|
||||
mux.HandleFunc("/static/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Allow access to CSS and JS files without auth (needed for login page)
|
||||
path := strings.TrimPrefix(r.URL.Path, "/static/")
|
||||
if strings.HasSuffix(path, ".css") || strings.HasSuffix(path, ".js") {
|
||||
// Strip the /static/ prefix before serving
|
||||
http.StripPrefix("/static/", staticHandler).ServeHTTP(rw, r)
|
||||
return
|
||||
}
|
||||
// For other static files, require auth
|
||||
server.authMiddleware(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Strip the /static/ prefix before serving
|
||||
http.StripPrefix("/static/", staticHandler).ServeHTTP(rw, r)
|
||||
})(rw, r)
|
||||
})
|
||||
}
|
||||
|
||||
// serveFile serves any file from embedded files or returns 404 if not found
|
||||
|
|
|
@ -19,7 +19,10 @@
|
|||
<p data-key="subtitle">Network mesh management dashboard</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="language-switcher">
|
||||
<div class="controls-group">
|
||||
<button onclick="toggleTheme()" class="theme-btn" id="theme-btn">
|
||||
<span class="theme-icon">🌙</span>
|
||||
</button>
|
||||
<button onclick="switchLanguage('ru')" class="lang-btn" id="lang-ru">RU</button>
|
||||
<button onclick="switchLanguage('en')" class="lang-btn" id="lang-en">EN</button>
|
||||
</div>
|
||||
|
@ -126,6 +129,7 @@
|
|||
|
||||
<script>
|
||||
let currentLanguage = localStorage.getItem('yggdrasil-language') || 'ru';
|
||||
let currentTheme = localStorage.getItem('yggdrasil-theme') || 'light';
|
||||
|
||||
function updateTexts() {
|
||||
const elements = document.querySelectorAll('[data-key]');
|
||||
|
@ -137,6 +141,28 @@
|
|||
});
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
localStorage.setItem('yggdrasil-theme', currentTheme);
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
function applyTheme() {
|
||||
const body = document.body;
|
||||
const themeIcon = document.querySelector('.theme-icon');
|
||||
const themeBtn = document.getElementById('theme-btn');
|
||||
|
||||
if (currentTheme === 'dark') {
|
||||
body.setAttribute('data-theme', 'dark');
|
||||
themeIcon.textContent = '☀️';
|
||||
themeBtn.title = window.translations[currentLanguage]['theme_light'] || 'Light theme';
|
||||
} else {
|
||||
body.removeAttribute('data-theme');
|
||||
themeIcon.textContent = '🌙';
|
||||
themeBtn.title = window.translations[currentLanguage]['theme_dark'] || 'Dark theme';
|
||||
}
|
||||
}
|
||||
|
||||
function switchLanguage(lang) {
|
||||
currentLanguage = lang;
|
||||
localStorage.setItem('yggdrasil-language', lang);
|
||||
|
@ -146,6 +172,7 @@
|
|||
document.getElementById('lang-' + lang).classList.add('active');
|
||||
|
||||
updateTexts();
|
||||
applyTheme(); // Update theme button tooltip
|
||||
}
|
||||
|
||||
function logout() {
|
||||
|
@ -174,12 +201,14 @@
|
|||
event.target.closest('.nav-item').classList.add('active');
|
||||
}
|
||||
|
||||
// Initialize language on page load
|
||||
// Initialize language and theme on page load
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Set active language button
|
||||
document.getElementById('lang-' + currentLanguage).classList.add('active');
|
||||
// Update all texts
|
||||
updateTexts();
|
||||
// Apply saved theme
|
||||
applyTheme();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
@ -29,5 +29,12 @@ window.translations.en = {
|
|||
'network_settings_description': 'Network interaction parameters',
|
||||
'coming_soon': 'Coming soon...',
|
||||
'footer_text': 'Yggdrasil Network • Minimal WebUI v1.0',
|
||||
'logout_confirm': 'Are you sure you want to logout?'
|
||||
'logout_confirm': 'Are you sure you want to logout?',
|
||||
'theme_light': 'Light theme',
|
||||
'theme_dark': 'Dark theme',
|
||||
'login_subtitle': 'Enter password to access the web interface',
|
||||
'password_label': 'Password:',
|
||||
'access_dashboard': 'Access Dashboard',
|
||||
'error_invalid_password': 'Invalid password. Please try again.',
|
||||
'error_too_many_attempts': 'Too many failed attempts. Please wait 1 minute before trying again.'
|
||||
};
|
|
@ -29,5 +29,12 @@ window.translations.ru = {
|
|||
'network_settings_description': 'Параметры сетевого взаимодействия',
|
||||
'coming_soon': 'Функция в разработке...',
|
||||
'footer_text': 'Yggdrasil Network • Minimal WebUI v1.0',
|
||||
'logout_confirm': 'Вы уверены, что хотите выйти?'
|
||||
'logout_confirm': 'Вы уверены, что хотите выйти?',
|
||||
'theme_light': 'Светлая тема',
|
||||
'theme_dark': 'Темная тема',
|
||||
'login_subtitle': 'Введите пароль для доступа к веб-интерфейсу',
|
||||
'password_label': 'Пароль:',
|
||||
'access_dashboard': 'Войти в панель',
|
||||
'error_invalid_password': 'Неверный пароль. Попробуйте снова.',
|
||||
'error_too_many_attempts': 'Слишком много неудачных попыток. Подождите 1 минуту перед повторной попыткой.'
|
||||
};
|
|
@ -6,7 +6,18 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Yggdrasil Web Interface - Login</title>
|
||||
<link rel="stylesheet" href="static/style.css">
|
||||
<script src="static/lang/ru.js"></script>
|
||||
<script src="static/lang/en.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
||||
min-height: 100vh;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -16,24 +27,27 @@
|
|||
}
|
||||
|
||||
.login-form {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
background: var(--bg-info-card);
|
||||
border-radius: 4px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 8px var(--shadow-dark);
|
||||
border: 1px solid var(--border-card);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-form h1 {
|
||||
color: #495057;
|
||||
color: var(--text-heading);
|
||||
margin-bottom: 10px;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.login-form p {
|
||||
color: #6c757d;
|
||||
color: var(--text-body);
|
||||
margin-bottom: 30px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
|
@ -44,95 +58,164 @@
|
|||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
color: var(--text-heading);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-card);
|
||||
border-radius: 2px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.2s ease;
|
||||
background: var(--bg-status-card);
|
||||
color: var(--text-body);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
border-color: var(--border-hover);
|
||||
box-shadow: 0 1px 4px var(--shadow-light);
|
||||
}
|
||||
|
||||
.form-group input:disabled {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #e9ecef;
|
||||
color: #6c757d;
|
||||
background-color: var(--bg-nav-item);
|
||||
border-color: var(--border-nav-item);
|
||||
color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--bg-nav-active);
|
||||
color: var(--text-white);
|
||||
border: 1px solid var(--bg-nav-active-border);
|
||||
padding: 12px 16px;
|
||||
border-radius: 2px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
transition: background 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: translateY(0);
|
||||
background: var(--bg-nav-active-border);
|
||||
box-shadow: 0 2px 8px var(--shadow-medium);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f1aeb5;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
background: var(--bg-status-card);
|
||||
color: var(--bg-logout);
|
||||
border: 1px solid var(--border-logout);
|
||||
border-radius: 2px;
|
||||
padding: 12px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 20px;
|
||||
color: #667eea;
|
||||
color: var(--bg-nav-active);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="login-header">
|
||||
<div class="controls-group">
|
||||
<button onclick="toggleTheme()" class="theme-btn" id="theme-btn">
|
||||
<span class="theme-icon">🌙</span>
|
||||
</button>
|
||||
<button onclick="switchLanguage('ru')" class="lang-btn" id="lang-ru">RU</button>
|
||||
<button onclick="switchLanguage('en')" class="lang-btn" id="lang-en">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-container">
|
||||
<form class="login-form" onsubmit="return handleLogin(event)">
|
||||
<div class="lock-icon">🔒</div>
|
||||
<h1>🌳 Yggdrasil</h1>
|
||||
<p>Enter password to access the web interface</p>
|
||||
<h1 data-key="title">🌳 Yggdrasil</h1>
|
||||
<p data-key="login_subtitle">Enter password to access the web interface</p>
|
||||
|
||||
<div class="error-message" id="errorMessage">
|
||||
Invalid password. Please try again.
|
||||
<span data-key="error_invalid_password">Invalid password. Please try again.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password:</label>
|
||||
<label for="password" data-key="password_label">Password:</label>
|
||||
<input type="password" id="password" name="password" required autofocus>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-button">
|
||||
<button type="submit" class="login-button" data-key="access_dashboard">
|
||||
Access Dashboard
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentLanguage = localStorage.getItem('yggdrasil-language') || 'ru';
|
||||
let currentTheme = localStorage.getItem('yggdrasil-theme') || 'light';
|
||||
|
||||
function updateTexts() {
|
||||
const elements = document.querySelectorAll('[data-key]');
|
||||
elements.forEach(element => {
|
||||
const key = element.getAttribute('data-key');
|
||||
if (window.translations && window.translations[currentLanguage] && window.translations[currentLanguage][key]) {
|
||||
element.textContent = window.translations[currentLanguage][key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
localStorage.setItem('yggdrasil-theme', currentTheme);
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
function applyTheme() {
|
||||
const body = document.body;
|
||||
const themeIcon = document.querySelector('.theme-icon');
|
||||
const themeBtn = document.getElementById('theme-btn');
|
||||
|
||||
if (currentTheme === 'dark') {
|
||||
body.setAttribute('data-theme', 'dark');
|
||||
themeIcon.textContent = '☀️';
|
||||
themeBtn.title = window.translations[currentLanguage]['theme_light'] || 'Light theme';
|
||||
} else {
|
||||
body.removeAttribute('data-theme');
|
||||
themeIcon.textContent = '🌙';
|
||||
themeBtn.title = window.translations[currentLanguage]['theme_dark'] || 'Dark theme';
|
||||
}
|
||||
}
|
||||
|
||||
function switchLanguage(lang) {
|
||||
currentLanguage = lang;
|
||||
localStorage.setItem('yggdrasil-language', lang);
|
||||
|
||||
// Update active button
|
||||
document.querySelectorAll('.lang-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.getElementById('lang-' + lang).classList.add('active');
|
||||
|
||||
updateTexts();
|
||||
applyTheme(); // Update theme button tooltip
|
||||
}
|
||||
|
||||
async function handleLogin(event) {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -153,7 +236,7 @@
|
|||
window.location.href = '/';
|
||||
} else if (response.status === 429) {
|
||||
// Too many requests - IP blocked
|
||||
errorMessage.textContent = 'Too many failed attempts. Please wait 1 minute before trying again.';
|
||||
errorMessage.querySelector('span').textContent = window.translations[currentLanguage]['error_too_many_attempts'] || 'Too many failed attempts. Please wait 1 minute before trying again.';
|
||||
errorMessage.style.display = 'block';
|
||||
document.getElementById('password').value = '';
|
||||
document.getElementById('password').disabled = true;
|
||||
|
@ -166,7 +249,7 @@
|
|||
}, 60000);
|
||||
} else {
|
||||
// Invalid password
|
||||
errorMessage.textContent = 'Invalid password. Please try again.';
|
||||
errorMessage.querySelector('span').textContent = window.translations[currentLanguage]['error_invalid_password'] || 'Invalid password. Please try again.';
|
||||
errorMessage.style.display = 'block';
|
||||
document.getElementById('password').value = '';
|
||||
document.getElementById('password').focus();
|
||||
|
@ -183,6 +266,16 @@
|
|||
document.getElementById('password').addEventListener('input', function () {
|
||||
document.getElementById('errorMessage').style.display = 'none';
|
||||
});
|
||||
|
||||
// Initialize language and theme on page load
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Set active language button
|
||||
document.getElementById('lang-' + currentLanguage).classList.add('active');
|
||||
// Update all texts
|
||||
updateTexts();
|
||||
// Apply saved theme
|
||||
applyTheme();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
|
|
@ -1,3 +1,91 @@
|
|||
/* Light theme (default) */
|
||||
:root {
|
||||
/* Background colors */
|
||||
--bg-primary: #667eea;
|
||||
--bg-secondary: #764ba2;
|
||||
--bg-sidebar: #ffffff;
|
||||
--bg-nav-item: #f8f9fa;
|
||||
--bg-nav-hover: #e9ecef;
|
||||
--bg-nav-active: #3498db;
|
||||
--bg-nav-active-border: #2980b9;
|
||||
--bg-main-content: #fafafa;
|
||||
--bg-status-card: #f5f5f5;
|
||||
--bg-info-card: #ffffff;
|
||||
--bg-logout: #dc3545;
|
||||
--bg-logout-hover: #c82333;
|
||||
--bg-lang-switcher: rgba(255, 255, 255, 0.2);
|
||||
--bg-lang-btn-hover: rgba(255, 255, 255, 0.3);
|
||||
--bg-lang-btn-active: rgba(255, 255, 255, 0.4);
|
||||
|
||||
/* Border colors */
|
||||
--border-sidebar: #e0e0e0;
|
||||
--border-nav-item: #dee2e6;
|
||||
--border-main: #e0e0e0;
|
||||
--border-card: #e0e0e0;
|
||||
--border-hover: #3498db;
|
||||
--border-footer: #dee2e6;
|
||||
--border-logout: #c82333;
|
||||
--border-lang: rgba(255, 255, 255, 0.3);
|
||||
|
||||
/* Text colors */
|
||||
--text-primary: #333;
|
||||
--text-white: white;
|
||||
--text-nav: #495057;
|
||||
--text-heading: #343a40;
|
||||
--text-body: #495057;
|
||||
--text-muted: #6c757d;
|
||||
|
||||
/* Shadow colors */
|
||||
--shadow-light: rgba(0, 0, 0, 0.1);
|
||||
--shadow-medium: rgba(0, 0, 0, 0.15);
|
||||
--shadow-dark: rgba(0, 0, 0, 0.2);
|
||||
--shadow-heavy: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
[data-theme="dark"] {
|
||||
/* Background colors */
|
||||
--bg-primary: #2c3e50;
|
||||
--bg-secondary: #34495e;
|
||||
--bg-sidebar: #37474f;
|
||||
--bg-nav-item: #455a64;
|
||||
--bg-nav-hover: #546e7a;
|
||||
--bg-nav-active: #3498db;
|
||||
--bg-nav-active-border: #2980b9;
|
||||
--bg-main-content: #37474f;
|
||||
--bg-status-card: #455a64;
|
||||
--bg-info-card: #455a64;
|
||||
--bg-logout: #dc3545;
|
||||
--bg-logout-hover: #c82333;
|
||||
--bg-lang-switcher: rgba(0, 0, 0, 0.3);
|
||||
--bg-lang-btn-hover: rgba(255, 255, 255, 0.1);
|
||||
--bg-lang-btn-active: rgba(255, 255, 255, 0.2);
|
||||
|
||||
/* Border colors */
|
||||
--border-sidebar: #455a64;
|
||||
--border-nav-item: #546e7a;
|
||||
--border-main: #455a64;
|
||||
--border-card: #546e7a;
|
||||
--border-hover: #3498db;
|
||||
--border-footer: #546e7a;
|
||||
--border-logout: #c82333;
|
||||
--border-lang: rgba(255, 255, 255, 0.3);
|
||||
|
||||
/* Text colors */
|
||||
--text-primary: #333;
|
||||
--text-white: white;
|
||||
--text-nav: #eceff1;
|
||||
--text-heading: #eceff1;
|
||||
--text-body: #cfd8dc;
|
||||
--text-muted: #b0bec5;
|
||||
|
||||
/* Shadow colors */
|
||||
--shadow-light: rgba(0, 0, 0, 0.2);
|
||||
--shadow-medium: rgba(0, 0, 0, 0.3);
|
||||
--shadow-dark: rgba(0, 0, 0, 0.4);
|
||||
--shadow-heavy: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@ -5,10 +93,10 @@
|
|||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.container {
|
||||
|
@ -19,7 +107,7 @@ body {
|
|||
|
||||
header {
|
||||
margin-bottom: 40px;
|
||||
color: white;
|
||||
color: var(--text-white);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
|
@ -35,33 +123,58 @@ header {
|
|||
gap: 15px;
|
||||
}
|
||||
|
||||
.language-switcher {
|
||||
.controls-group {
|
||||
display: flex;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: var(--bg-lang-switcher);
|
||||
border-radius: 2px;
|
||||
padding: 2px;
|
||||
border: 1px solid var(--border-lang);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.theme-btn {
|
||||
background: transparent;
|
||||
color: var(--text-white);
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 1px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.theme-btn:hover {
|
||||
background: var(--bg-lang-btn-hover);
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
display: block;
|
||||
transition: transform 0.3s ease;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.lang-btn {
|
||||
background: transparent;
|
||||
color: white;
|
||||
color: var(--text-white);
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
border-radius: 1px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.lang-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: var(--bg-lang-btn-hover);
|
||||
}
|
||||
|
||||
.lang-btn.active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
background: var(--bg-lang-btn-active);
|
||||
}
|
||||
|
||||
.header-content > div:first-child {
|
||||
|
@ -70,9 +183,11 @@ header {
|
|||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
text-shadow: 1px 1px 2px var(--shadow-heavy);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
header p {
|
||||
|
@ -83,20 +198,20 @@ header p {
|
|||
.sidebar-footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-top: 1px solid var(--border-footer);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
background: rgba(220, 53, 69, 0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(220, 53, 69, 0.3);
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
background: var(--bg-logout);
|
||||
color: var(--text-white);
|
||||
border: 1px solid var(--border-logout);
|
||||
padding: 10px 16px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -104,9 +219,7 @@ header p {
|
|||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: rgba(220, 53, 69, 0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(220, 53, 69, 0.2);
|
||||
background: var(--bg-logout-hover);
|
||||
}
|
||||
|
||||
.logout-btn:before {
|
||||
|
@ -122,12 +235,11 @@ header p {
|
|||
|
||||
.sidebar {
|
||||
min-width: 250px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
background: var(--bg-sidebar);
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
box-shadow: 0 2px 8px var(--shadow-heavy);
|
||||
border: 1px solid var(--border-sidebar);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
@ -143,25 +255,24 @@ header p {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 15px 18px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-nav-item);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: background 0.2s ease;
|
||||
color: var(--text-nav);
|
||||
border: 1px solid var(--border-nav-item);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
background: var(--bg-nav-hover);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
background: var(--bg-nav-active);
|
||||
border-color: var(--bg-nav-active-border);
|
||||
color: var(--text-white);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
|
@ -175,12 +286,11 @@ header p {
|
|||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 12px;
|
||||
background: var(--bg-main-content);
|
||||
border-radius: 4px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
box-shadow: 0 2px 8px var(--shadow-dark);
|
||||
border: 1px solid var(--border-main);
|
||||
}
|
||||
|
||||
.content-section {
|
||||
|
@ -192,13 +302,25 @@ header p {
|
|||
}
|
||||
|
||||
.status-card {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
background: var(--bg-status-card);
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0 1px 4px var(--shadow-light);
|
||||
border: 1px solid var(--border-card);
|
||||
}
|
||||
|
||||
.status-card h2 {
|
||||
color: var(--text-heading);
|
||||
font-weight: 700;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.status-card p {
|
||||
color: var(--text-body);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
|
@ -213,11 +335,11 @@ header p {
|
|||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #6c757d;
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-dot.active {
|
||||
background: #28a745;
|
||||
background: var(--bg-nav-active);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
|
@ -234,39 +356,44 @@ header p {
|
|||
}
|
||||
|
||||
.info-card {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
border: 1px solid rgba(233, 236, 239, 0.6);
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
background: var(--bg-info-card);
|
||||
border: 1px solid var(--border-card);
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
transition: box-shadow 0.2s ease;
|
||||
box-shadow: 0 1px 4px var(--shadow-light);
|
||||
}
|
||||
|
||||
.info-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
||||
border-color: rgba(102, 126, 234, 0.3);
|
||||
box-shadow: 0 2px 8px var(--shadow-medium);
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
color: #495057;
|
||||
color: var(--text-heading);
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.info-card p {
|
||||
color: #6c757d;
|
||||
color: var(--text-body);
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-card small {
|
||||
color: #adb5bd;
|
||||
font-style: italic;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
color: white;
|
||||
color: var(--text-white);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
@ -310,7 +437,7 @@ footer {
|
|||
margin-left: 15px;
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-left: 1px solid var(--border-footer);
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue