feat: Service-Editor mit vollem CRUD und Server-Speichern
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 16s
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 16s
This commit is contained in:
12
admin.html
12
admin.html
@@ -12,7 +12,7 @@
|
|||||||
<link rel="icon" type="image/png" sizes="48x48" href="/icons/favicon-48-v4.png">
|
<link rel="icon" type="image/png" sizes="48x48" href="/icons/favicon-48-v4.png">
|
||||||
<link rel="icon" type="image/png" sizes="192x192" href="/icons/favicon-192-v4.png">
|
<link rel="icon" type="image/png" sizes="192x192" href="/icons/favicon-192-v4.png">
|
||||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon-v4.png">
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon-v4.png">
|
||||||
<link rel="stylesheet" href="/style.css?v=34">
|
<link rel="stylesheet" href="/style.css?v=35">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="shell hero admin-hero">
|
<header class="shell hero admin-hero">
|
||||||
@@ -147,13 +147,15 @@
|
|||||||
<div class="ops-card-head">
|
<div class="ops-card-head">
|
||||||
<span>Dienste & Kategorien (Konfiguration)</span>
|
<span>Dienste & Kategorien (Konfiguration)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-actions-bar">
|
<div class="admin-actions-bar" style="display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px;">
|
||||||
<button class="ghost-button mini" id="checkAllButton" type="button">Alle Status pruefen</button>
|
<button class="ghost-button mini" id="checkAllButton" type="button">Alle Status pruefen</button>
|
||||||
<button class="ghost-button mini" id="saveLocalButton" type="button">Lokal speichern</button>
|
<button class="ghost-button mini" id="addServiceButton" type="button">Neuen Dienst hinzufügen</button>
|
||||||
|
<button class="ghost-button mini" id="saveServerButton" type="button" style="background: rgba(56, 189, 248, 0.12); border-color: rgba(56, 189, 248, 0.35); color: #38bdf8;">Auf Server speichern</button>
|
||||||
<button class="ghost-button mini" id="resetLocalButton" type="button">Aenderungen verwerfen</button>
|
<button class="ghost-button mini" id="resetLocalButton" type="button">Aenderungen verwerfen</button>
|
||||||
<button class="ghost-button mini" id="downloadConfigButton" type="button">JSON exportieren</button>
|
<button class="ghost-button mini" id="downloadConfigButton" type="button">JSON exportieren</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-table" id="serviceEditor"></div>
|
<div id="editorFeedback" class="notice" style="display: none; margin-bottom: 16px;"></div>
|
||||||
|
<div class="admin-table" id="serviceEditor" style="display: flex; flex-direction: column; gap: 16px;"></div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -242,6 +244,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/admin.js?v=30" defer></script>
|
<script src="/admin.js?v=31" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
230
admin.js
230
admin.js
@@ -17,7 +17,9 @@ const loginMessage = document.querySelector('#loginMessage');
|
|||||||
const adminUser = document.querySelector('#adminUser');
|
const adminUser = document.querySelector('#adminUser');
|
||||||
const serviceEditor = document.querySelector('#serviceEditor');
|
const serviceEditor = document.querySelector('#serviceEditor');
|
||||||
const checkAllButton = document.querySelector('#checkAllButton');
|
const checkAllButton = document.querySelector('#checkAllButton');
|
||||||
const saveLocalButton = document.querySelector('#saveLocalButton');
|
const addServiceButton = document.querySelector('#addServiceButton');
|
||||||
|
const saveServerButton = document.querySelector('#saveServerButton');
|
||||||
|
const editorFeedback = document.querySelector('#editorFeedback');
|
||||||
const resetLocalButton = document.querySelector('#resetLocalButton');
|
const resetLocalButton = document.querySelector('#resetLocalButton');
|
||||||
const downloadConfigButton = document.querySelector('#downloadConfigButton');
|
const downloadConfigButton = document.querySelector('#downloadConfigButton');
|
||||||
const refreshOpsButton = document.querySelector('#refreshOpsButton');
|
const refreshOpsButton = document.querySelector('#refreshOpsButton');
|
||||||
@@ -193,41 +195,128 @@ function categoryOptions(selected) {
|
|||||||
)).join('');
|
)).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function iconOptions(selected) {
|
||||||
|
const icons = ['shield', 'folder', 'lock', 'film', 'headphones', 'book', 'git', 'chess', 'user'];
|
||||||
|
return icons.map((icon) => (
|
||||||
|
`<option value="${icon}" ${icon === selected ? 'selected' : ''}>${icon}</option>`
|
||||||
|
)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
function renderEditor() {
|
function renderEditor() {
|
||||||
serviceEditor.innerHTML = config.services.map((service, index) => `
|
serviceEditor.innerHTML = config.services.map((service, index) => `
|
||||||
<article class="admin-row" data-index="${index}">
|
<article class="admin-row" data-index="${index}">
|
||||||
<div>
|
<!-- Group 1: Identitaet & Kategorie -->
|
||||||
<strong>${service.name}</strong>
|
<div class="admin-form-group-row">
|
||||||
<span>${service.domain}</span>
|
<label style="flex: 1 1 120px;">
|
||||||
</div>
|
Dienst ID (Eindeutig)
|
||||||
<label>
|
<input data-field="id" value="${service.id || ''}" placeholder="z.B. passwort-manager">
|
||||||
|
</label>
|
||||||
|
<label style="flex: 2 2 180px;">
|
||||||
|
Anzeigename
|
||||||
|
<input data-field="name" value="${service.name || ''}" placeholder="z.B. Passwords">
|
||||||
|
</label>
|
||||||
|
<label style="flex: 2 2 200px;">
|
||||||
|
Subdomain / Domain
|
||||||
|
<input data-field="domain" value="${service.domain || ''}" placeholder="z.B. password.mischlabs.de">
|
||||||
|
</label>
|
||||||
|
<label style="flex: 1.5 1.5 150px;">
|
||||||
Kategorie
|
Kategorie
|
||||||
<select data-field="category">${categoryOptions(service.category)}</select>
|
<select data-field="category">${categoryOptions(service.category)}</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
</div>
|
||||||
URL
|
|
||||||
<input data-field="url" value="${service.url}">
|
<!-- Group 2: URLs, Aesthetics & Icon -->
|
||||||
|
<div class="admin-form-group-row">
|
||||||
|
<label style="flex: 3 3 250px;">
|
||||||
|
Dienst-URL (Zieladresse)
|
||||||
|
<input data-field="url" value="${service.url || ''}" placeholder="z.B. https://password.mischlabs.de">
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label style="flex: 2 2 180px;">
|
||||||
Mobile URL (App Scheme)
|
Mobile URL Scheme (Optional)
|
||||||
<input data-field="mobileUrl" value="${service.mobileUrl || ''}" placeholder="z.B. nextcloud://">
|
<input data-field="mobileUrl" value="${service.mobileUrl || ''}" placeholder="z.B. bitwarden://">
|
||||||
</label>
|
</label>
|
||||||
<button class="ghost-button check-one" type="button">Pruefen</button>
|
<label style="flex: 1.5 1.5 140px;">
|
||||||
|
Accent-Farbe
|
||||||
|
<div style="display: flex; gap: 6px; align-items: center;">
|
||||||
|
<input type="color" class="accent-color-picker" value="${service.accent || '#38bdf8'}" style="width: 42px; min-width: 42px; height: 38px; padding: 2px; cursor: pointer; border: 1px solid var(--border); border-radius: var(--radius); background: rgba(17,22,36,0.92);">
|
||||||
|
<input data-field="accent" value="${service.accent || '#38bdf8'}" placeholder="#ffffff" style="font-family: monospace;">
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label style="flex: 1.2 1.2 120px;">
|
||||||
|
Icon
|
||||||
|
<select data-field="icon">${iconOptions(service.icon)}</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Group 3: Description, Keywords & Actions -->
|
||||||
|
<div class="admin-form-group-row">
|
||||||
|
<label style="flex: 2 2 250px;">
|
||||||
|
Beschreibung
|
||||||
|
<input data-field="description" value="${service.description || ''}" placeholder="Kurze Dienstbeschreibung...">
|
||||||
|
</label>
|
||||||
|
<label style="flex: 2 2 250px;">
|
||||||
|
Keywords (Suche)
|
||||||
|
<input data-field="keywords" value="${service.keywords || ''}" placeholder="Schluesselwoerter (durch Leerzeichen getrennt)...">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-row-actions">
|
||||||
|
<button class="ghost-button check-one mini" type="button">Verbindung Pruefen</button>
|
||||||
|
<button class="ghost-button danger-button delete-one mini" type="button" style="border-color: rgba(251, 113, 133, 0.3); color: #fb7185;">Loeschen</button>
|
||||||
|
</div>
|
||||||
<div class="diagnostic">Noch nicht geprueft.</div>
|
<div class="diagnostic">Noch nicht geprueft.</div>
|
||||||
</article>
|
</article>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
serviceEditor.querySelectorAll('select,input').forEach((input) => {
|
// Wire up two-way sync for color pickers and text inputs, and event listeners
|
||||||
|
serviceEditor.querySelectorAll('.admin-row').forEach((row) => {
|
||||||
|
const index = Number(row.dataset.index);
|
||||||
|
const service = config.services[index];
|
||||||
|
|
||||||
|
// Wire standard input changes
|
||||||
|
row.querySelectorAll('select, input:not(.accent-color-picker)').forEach((input) => {
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
service[input.dataset.field] = input.value;
|
||||||
|
});
|
||||||
input.addEventListener('change', () => {
|
input.addEventListener('change', () => {
|
||||||
const row = input.closest('.admin-row');
|
|
||||||
const service = config.services[Number(row.dataset.index)];
|
|
||||||
service[input.dataset.field] = input.value;
|
service[input.dataset.field] = input.value;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Wire color picker sync
|
||||||
|
const textInput = row.querySelector('input[data-field="accent"]');
|
||||||
|
const colorPicker = row.querySelector('.accent-color-picker');
|
||||||
|
|
||||||
|
if (textInput && colorPicker) {
|
||||||
|
colorPicker.addEventListener('input', () => {
|
||||||
|
textInput.value = colorPicker.value;
|
||||||
|
service.accent = colorPicker.value;
|
||||||
|
});
|
||||||
|
textInput.addEventListener('input', () => {
|
||||||
|
const val = textInput.value;
|
||||||
|
if (/^#[0-9A-Fa-f]{6}$/.test(val)) {
|
||||||
|
colorPicker.value = val;
|
||||||
|
}
|
||||||
|
service.accent = val;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
serviceEditor.querySelectorAll('.check-one').forEach((button) => {
|
serviceEditor.querySelectorAll('.check-one').forEach((button) => {
|
||||||
button.addEventListener('click', () => checkRow(button.closest('.admin-row')));
|
button.addEventListener('click', () => checkRow(button.closest('.admin-row')));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
serviceEditor.querySelectorAll('.delete-one').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const row = button.closest('.admin-row');
|
||||||
|
const index = Number(row.dataset.index);
|
||||||
|
const service = config.services[index];
|
||||||
|
const confirmed = window.confirm(`Dienst "${service.name || service.id || 'unbekannt'}" wirklich loeschen?`);
|
||||||
|
if (!confirmed) return;
|
||||||
|
config.services.splice(index, 1);
|
||||||
|
renderEditor();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function probe(url) {
|
async function probe(url) {
|
||||||
@@ -282,8 +371,102 @@ async function checkRow(row) {
|
|||||||
diagnostic.textContent = `${label} - ${result.ms} ms - ${result.detail}`;
|
diagnostic.textContent = `${label} - ${result.ms} ms - ${result.detail}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveLocal() {
|
function addService() {
|
||||||
localStorage.setItem(LOCAL_CONFIG_KEY, JSON.stringify(config, null, 2));
|
const newService = {
|
||||||
|
id: `dienst-${Date.now()}`,
|
||||||
|
name: 'Neuer Dienst',
|
||||||
|
description: 'Beschreibung des Dienstes',
|
||||||
|
domain: 'neu.mischlabs.de',
|
||||||
|
url: 'https://neu.mischlabs.de',
|
||||||
|
category: config.categories[0]?.id || 'access',
|
||||||
|
accent: '#38bdf8',
|
||||||
|
icon: 'shield',
|
||||||
|
keywords: 'neu dienst'
|
||||||
|
};
|
||||||
|
config.services.push(newService);
|
||||||
|
renderEditor();
|
||||||
|
|
||||||
|
// Scroll new row into view smoothly
|
||||||
|
const rows = serviceEditor.querySelectorAll('.admin-row');
|
||||||
|
const lastRow = rows[rows.length - 1];
|
||||||
|
if (lastRow) {
|
||||||
|
lastRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
lastRow.classList.add('is-new');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFeedback(type, message) {
|
||||||
|
if (!editorFeedback) return;
|
||||||
|
editorFeedback.style.display = 'block';
|
||||||
|
editorFeedback.textContent = message;
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
editorFeedback.style.background = 'rgba(52, 211, 153, 0.12)';
|
||||||
|
editorFeedback.style.borderColor = 'rgba(52, 211, 153, 0.35)';
|
||||||
|
editorFeedback.style.color = '#34d399';
|
||||||
|
} else {
|
||||||
|
editorFeedback.style.background = 'rgba(251, 113, 133, 0.12)';
|
||||||
|
editorFeedback.style.borderColor = 'rgba(251, 113, 133, 0.35)';
|
||||||
|
editorFeedback.style.color = '#fb7185';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
setTimeout(() => {
|
||||||
|
editorFeedback.style.display = 'none';
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveServer() {
|
||||||
|
if (!tokenSet?.id_token) {
|
||||||
|
showFeedback('error', 'Nicht angemeldet oder Sitzung abgelaufen.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config || !Array.isArray(config.services) || !Array.isArray(config.categories)) {
|
||||||
|
showFeedback('error', 'Ungueltiges Konfigurationsformat.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const service of config.services) {
|
||||||
|
if (!service.id || !service.name || !service.domain || !service.url || !service.category) {
|
||||||
|
showFeedback('error', 'Bitte füllen Sie ID, Name, Domain, URL und Kategorie für alle Dienste aus.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalText = saveServerButton.textContent;
|
||||||
|
saveServerButton.disabled = true;
|
||||||
|
saveServerButton.textContent = 'Speichere...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/services/save', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${tokenSet.id_token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(config)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
showFeedback('success', 'Konfiguration erfolgreich auf dem Server gespeichert!');
|
||||||
|
localStorage.removeItem(LOCAL_CONFIG_KEY);
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
refreshOps();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showFeedback('error', `Speichern fehlgeschlagen: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
saveServerButton.disabled = false;
|
||||||
|
saveServerButton.textContent = originalText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadConfig() {
|
function downloadConfig() {
|
||||||
@@ -1278,10 +1461,13 @@ checkAllButton.addEventListener('click', () => {
|
|||||||
[...serviceEditor.querySelectorAll('.admin-row')].forEach((row) => checkRow(row));
|
[...serviceEditor.querySelectorAll('.admin-row')].forEach((row) => checkRow(row));
|
||||||
});
|
});
|
||||||
|
|
||||||
saveLocalButton.addEventListener('click', () => {
|
if (addServiceButton) {
|
||||||
saveLocal();
|
addServiceButton.addEventListener('click', addService);
|
||||||
window.alert('Lokal gespeichert. Das Dashboard in diesem Browser nutzt diese Aenderungen.');
|
}
|
||||||
});
|
|
||||||
|
if (saveServerButton) {
|
||||||
|
saveServerButton.addEventListener('click', saveServer);
|
||||||
|
}
|
||||||
|
|
||||||
resetLocalButton.addEventListener('click', () => {
|
resetLocalButton.addEventListener('click', () => {
|
||||||
localStorage.removeItem(LOCAL_CONFIG_KEY);
|
localStorage.removeItem(LOCAL_CONFIG_KEY);
|
||||||
|
|||||||
37
server.js
37
server.js
@@ -1218,6 +1218,43 @@ const server = http.createServer(async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.url?.startsWith('/api/services/save')) {
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
json(res, 405, { error: 'Method not allowed' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await verifyAdminRequest(req);
|
||||||
|
|
||||||
|
const body = await new Promise((resolve, reject) => {
|
||||||
|
let data = '';
|
||||||
|
req.on('data', chunk => data += chunk);
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(data));
|
||||||
|
} catch (e) {
|
||||||
|
reject(new Error('Invalid JSON'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!body || !Array.isArray(body.categories) || !Array.isArray(body.services)) {
|
||||||
|
json(res, 400, { error: 'Invalid configuration format' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const servicesPath = path.join(PUBLIC_DIR, 'services.json');
|
||||||
|
await fsp.writeFile(servicesPath, JSON.stringify(body, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
json(res, 200, { success: true, message: 'Configuration saved successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
json(res, error.statusCode || 500, { error: error.message });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (req.url?.startsWith('/api/disks/scan-folder')) {
|
if (req.url?.startsWith('/api/disks/scan-folder')) {
|
||||||
if (req.method !== 'GET') {
|
if (req.method !== 'GET') {
|
||||||
json(res, 405, { error: 'Method not allowed' });
|
json(res, 405, { error: 'Method not allowed' });
|
||||||
|
|||||||
52
style.css
52
style.css
@@ -1016,14 +1016,15 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-row {
|
.admin-row {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: minmax(140px, 1fr) minmax(120px, 0.6fr) minmax(180px, 1.1fr) minmax(130px, 0.9fr) auto;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
align-items: end;
|
padding: 16px;
|
||||||
padding: 12px;
|
background: rgba(17, 22, 36, 0.42);
|
||||||
background: rgba(7, 9, 20, 0.42);
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-row > div:first-child {
|
.admin-row > div:first-child {
|
||||||
@@ -1945,3 +1946,42 @@ main {
|
|||||||
background: rgba(255, 255, 255, 0.18) !important;
|
background: rgba(255, 255, 255, 0.18) !important;
|
||||||
transform: scale(1.15);
|
transform: scale(1.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Service Editor Advanced Styling */
|
||||||
|
.admin-form-group-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-form-group-row > label {
|
||||||
|
flex: 1 1 200px;
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-row-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 4px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-row.is-new {
|
||||||
|
animation: new-row-pulse 2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes new-row-pulse {
|
||||||
|
0% {
|
||||||
|
border-color: #38bdf8;
|
||||||
|
box-shadow: 0 0 12px rgba(56, 189, 248, 0.4);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
border-color: var(--border);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user