2026-01-27 16:21:58 +00:00
|
|
|
---
|
|
|
|
|
export const prerender = false;
|
|
|
|
|
import Layout from '../../../layouts/Layout.astro';
|
|
|
|
|
import { API_BASE as apiBase } from '../../../lib/api';
|
|
|
|
|
const { slug } = Astro.params;
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
<Layout title="Community Plugins">
|
2026-01-29 16:52:30 +00:00
|
|
|
<section class="ui-page">
|
|
|
|
|
<div class="ui-container">
|
|
|
|
|
<div class="hero ui-card ui-card-glass">
|
|
|
|
|
<div class="hero-top">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 class="hero-title">Plugins</h1>
|
|
|
|
|
<p class="hero-subtitle">Manage plugins for this community</p>
|
|
|
|
|
</div>
|
|
|
|
|
<a class="ui-btn ui-btn-secondary" href={`/communities/${slug}`}>Back</a>
|
|
|
|
|
</div>
|
2026-01-27 16:21:58 +00:00
|
|
|
</div>
|
|
|
|
|
|
2026-01-29 16:52:30 +00:00
|
|
|
<div id="plugins-content">
|
|
|
|
|
<div class="state-card ui-card"><p class="loading">Loading...</p></div>
|
|
|
|
|
</div>
|
2026-01-27 16:21:58 +00:00
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</Layout>
|
|
|
|
|
|
|
|
|
|
<script define:vars={{ slug, apiBase }}>
|
|
|
|
|
const token = localStorage.getItem('token');
|
|
|
|
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
window.location.href = '/login';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escapeHtml(str) {
|
|
|
|
|
return String(str)
|
|
|
|
|
.replaceAll('&', '&')
|
|
|
|
|
.replaceAll('<', '<')
|
|
|
|
|
.replaceAll('>', '>')
|
|
|
|
|
.replaceAll('"', '"')
|
|
|
|
|
.replaceAll("'", ''');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchCommunityBySlug() {
|
|
|
|
|
const res = await fetch(`${apiBase}/api/communities`);
|
|
|
|
|
if (!res.ok) throw new Error('Failed to load communities');
|
|
|
|
|
const communities = await res.json();
|
|
|
|
|
return communities.find(c => c.slug === slug);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchMembership(communityId) {
|
|
|
|
|
const res = await fetch(`${apiBase}/api/communities/${communityId}/membership`, {
|
|
|
|
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!res.ok) throw new Error('Failed to check membership');
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchPlugins(communityId) {
|
|
|
|
|
const res = await fetch(`${apiBase}/api/communities/${communityId}/plugins`, {
|
|
|
|
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const err = await res.text();
|
|
|
|
|
throw new Error(err || 'Failed to load plugins');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderSchemaField(pluginName, key, propSchema, currentValue) {
|
|
|
|
|
const fieldId = `field-${pluginName}-${key}`;
|
|
|
|
|
const title = propSchema.title || key;
|
|
|
|
|
const desc = propSchema.description || '';
|
|
|
|
|
const type = propSchema.type || 'string';
|
|
|
|
|
const defaultVal = propSchema.default;
|
|
|
|
|
const value = currentValue !== undefined ? currentValue : defaultVal;
|
|
|
|
|
|
|
|
|
|
let input = '';
|
|
|
|
|
|
|
|
|
|
if (type === 'boolean') {
|
|
|
|
|
input = `
|
|
|
|
|
<label class="checkbox-label">
|
|
|
|
|
<input type="checkbox" id="${fieldId}" data-key="${key}" data-type="boolean" ${value ? 'checked' : ''} />
|
|
|
|
|
<span>${escapeHtml(title)}</span>
|
|
|
|
|
</label>
|
|
|
|
|
`;
|
|
|
|
|
} else if (type === 'integer' || type === 'number') {
|
|
|
|
|
const min = propSchema.minimum !== undefined ? `min="${propSchema.minimum}"` : '';
|
|
|
|
|
const max = propSchema.maximum !== undefined ? `max="${propSchema.maximum}"` : '';
|
|
|
|
|
input = `
|
|
|
|
|
<label for="${fieldId}">${escapeHtml(title)}</label>
|
|
|
|
|
<input type="number" id="${fieldId}" data-key="${key}" data-type="${type}" value="${value ?? ''}" ${min} ${max} />
|
|
|
|
|
`;
|
|
|
|
|
} else if (type === 'string' && propSchema.enum) {
|
|
|
|
|
const options = propSchema.enum.map(opt =>
|
|
|
|
|
`<option value="${escapeHtml(opt)}" ${value === opt ? 'selected' : ''}>${escapeHtml(opt)}</option>`
|
|
|
|
|
).join('');
|
|
|
|
|
input = `
|
|
|
|
|
<label for="${fieldId}">${escapeHtml(title)}</label>
|
|
|
|
|
<select id="${fieldId}" data-key="${key}" data-type="string">${options}</select>
|
|
|
|
|
`;
|
|
|
|
|
} else {
|
|
|
|
|
input = `
|
|
|
|
|
<label for="${fieldId}">${escapeHtml(title)}</label>
|
|
|
|
|
<input type="text" id="${fieldId}" data-key="${key}" data-type="string" value="${escapeHtml(value ?? '')}" />
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<div class="schema-field">
|
|
|
|
|
${input}
|
|
|
|
|
${desc ? `<p class="field-hint">${escapeHtml(desc)}</p>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderSettingsForm(pluginName, schema, settings) {
|
|
|
|
|
if (!schema || !schema.properties) {
|
|
|
|
|
const settingsText = JSON.stringify(settings ?? {}, null, 2);
|
|
|
|
|
return `
|
|
|
|
|
<div class="settings-body">
|
|
|
|
|
<p class="settings-hint">No schema defined. Edit raw JSON:</p>
|
|
|
|
|
<textarea class="settings-json" spellcheck="false" data-name="${escapeHtml(pluginName)}">${escapeHtml(settingsText)}</textarea>
|
|
|
|
|
<div class="settings-actions">
|
2026-01-29 19:12:18 +00:00
|
|
|
<button class="ui-btn ui-btn-primary js-save-json" data-name="${escapeHtml(pluginName)}">Save Settings</button>
|
2026-01-27 16:21:58 +00:00
|
|
|
<span class="status" id="status-${escapeHtml(pluginName)}"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fields = Object.entries(schema.properties).map(([key, propSchema]) => {
|
|
|
|
|
return renderSchemaField(pluginName, key, propSchema, settings?.[key]);
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<form class="settings-form" data-plugin="${escapeHtml(pluginName)}">
|
|
|
|
|
${fields}
|
|
|
|
|
<div class="settings-actions">
|
2026-01-29 16:52:30 +00:00
|
|
|
<button type="submit" class="ui-btn ui-btn-primary">Save Settings</button>
|
2026-01-27 16:21:58 +00:00
|
|
|
<span class="status" id="status-${escapeHtml(pluginName)}"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPlugins(community, membership, plugins) {
|
|
|
|
|
const container = document.getElementById('plugins-content');
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
const allowed = membership?.role === 'admin' || membership?.role === 'moderator';
|
|
|
|
|
|
|
|
|
|
if (!allowed) {
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
<div class="error">
|
|
|
|
|
<h2>Forbidden</h2>
|
|
|
|
|
<p>You must be an admin or moderator to manage plugins for ${escapeHtml(community.name)}.</p>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
container.innerHTML = `
|
2026-01-29 16:52:30 +00:00
|
|
|
<div class="community-badge ui-card">
|
|
|
|
|
<div class="community-badge-title">
|
|
|
|
|
<span class="name">${escapeHtml(community.name)}</span>
|
|
|
|
|
<span class="slug">/${escapeHtml(community.slug)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="community-badge-subtitle">Community plugin settings</div>
|
2026-01-27 16:21:58 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="plugins-list">
|
|
|
|
|
${plugins.map(p => {
|
|
|
|
|
const disabledGlobally = !p.global_is_active;
|
|
|
|
|
const isCore = !!p.is_core;
|
|
|
|
|
const checked = !!p.community_is_active;
|
|
|
|
|
const toggleDisabled = disabledGlobally || isCore;
|
|
|
|
|
const hasSchema = p.settings_schema && p.settings_schema.properties;
|
|
|
|
|
|
|
|
|
|
return `
|
2026-01-29 16:52:30 +00:00
|
|
|
<div class="plugin-card ui-card" data-name="${escapeHtml(p.name)}">
|
2026-01-27 16:21:58 +00:00
|
|
|
<div class="plugin-head">
|
|
|
|
|
<div class="plugin-title">
|
|
|
|
|
<h3>${escapeHtml(p.name)}</h3>
|
|
|
|
|
<span class="version">v${escapeHtml(p.version)}</span>
|
2026-01-29 16:52:30 +00:00
|
|
|
${isCore ? `<span class="ui-pill tag-core">core</span>` : ''}
|
|
|
|
|
${disabledGlobally ? `<span class="ui-pill tag-disabled">disabled globally</span>` : ''}
|
2026-01-27 16:21:58 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="plugin-controls">
|
|
|
|
|
<label class="switch" title="Enable/disable">
|
|
|
|
|
<input class="plugin-toggle" type="checkbox" data-name="${escapeHtml(p.name)}" ${checked ? 'checked' : ''} ${toggleDisabled ? 'disabled' : ''} />
|
|
|
|
|
<span class="slider"></span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
${p.description ? `<p class="desc">${escapeHtml(p.description)}</p>` : ''}
|
|
|
|
|
|
2026-01-29 16:52:30 +00:00
|
|
|
<details class="panel ui-card settings-panel">
|
|
|
|
|
<summary class="panel-summary">
|
|
|
|
|
<span class="panel-title">Settings</span>
|
|
|
|
|
<span class="panel-meta">${hasSchema ? 'Schema' : '<span class="ui-badge">JSON</span>'}</span>
|
|
|
|
|
</summary>
|
|
|
|
|
<div class="panel-body settings-body-wrap">
|
2026-01-27 16:21:58 +00:00
|
|
|
${renderSettingsForm(p.name, p.settings_schema, p.settings)}
|
|
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.plugin-toggle').forEach(el => {
|
|
|
|
|
el.addEventListener('change', async (e) => {
|
|
|
|
|
const input = e.target;
|
|
|
|
|
const pluginName = input.dataset.name;
|
|
|
|
|
const next = input.checked;
|
|
|
|
|
|
|
|
|
|
input.disabled = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${apiBase}/api/communities/${community.id}/plugins/${pluginName}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'Authorization': `Bearer ${token}`,
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ is_active: next }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const err = await res.text();
|
|
|
|
|
input.checked = !next;
|
|
|
|
|
alert(err || 'Failed to update plugin');
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
input.checked = !next;
|
|
|
|
|
alert('Failed to update plugin');
|
|
|
|
|
} finally {
|
|
|
|
|
input.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-29 19:12:18 +00:00
|
|
|
document.querySelectorAll('.js-save-json').forEach(el => {
|
2026-01-27 16:21:58 +00:00
|
|
|
el.addEventListener('click', async (e) => {
|
|
|
|
|
const btn = e.target;
|
|
|
|
|
const pluginName = btn.dataset.name;
|
|
|
|
|
const textarea = document.querySelector(`textarea.settings-json[data-name="${pluginName}"]`);
|
|
|
|
|
const statusEl = document.getElementById(`status-${pluginName}`);
|
|
|
|
|
|
|
|
|
|
if (!textarea) return;
|
|
|
|
|
|
|
|
|
|
let settings;
|
|
|
|
|
try {
|
|
|
|
|
settings = JSON.parse(textarea.value || '{}');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (statusEl) statusEl.textContent = 'Invalid JSON';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
if (statusEl) statusEl.textContent = 'Saving...';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${apiBase}/api/communities/${community.id}/plugins/${pluginName}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'Authorization': `Bearer ${token}`,
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ settings }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
if (statusEl) statusEl.textContent = 'Saved';
|
|
|
|
|
setTimeout(() => { if (statusEl) statusEl.textContent = ''; }, 1500);
|
|
|
|
|
} else {
|
|
|
|
|
const err = await res.text();
|
|
|
|
|
if (statusEl) statusEl.textContent = 'Error';
|
|
|
|
|
alert(err || 'Failed to save settings');
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (statusEl) statusEl.textContent = 'Error';
|
|
|
|
|
alert('Failed to save settings');
|
|
|
|
|
} finally {
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.settings-form').forEach(form => {
|
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const pluginName = form.dataset.plugin;
|
|
|
|
|
const statusEl = document.getElementById(`status-${pluginName}`);
|
|
|
|
|
const btn = form.querySelector('button[type="submit"]');
|
|
|
|
|
|
|
|
|
|
const settings = {};
|
|
|
|
|
form.querySelectorAll('[data-key]').forEach(input => {
|
|
|
|
|
const key = input.dataset.key;
|
|
|
|
|
const type = input.dataset.type;
|
|
|
|
|
|
|
|
|
|
if (type === 'boolean') {
|
|
|
|
|
settings[key] = input.checked;
|
|
|
|
|
} else if (type === 'integer') {
|
|
|
|
|
settings[key] = input.value ? parseInt(input.value, 10) : null;
|
|
|
|
|
} else if (type === 'number') {
|
|
|
|
|
settings[key] = input.value ? parseFloat(input.value) : null;
|
|
|
|
|
} else {
|
|
|
|
|
settings[key] = input.value;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (btn) btn.disabled = true;
|
|
|
|
|
if (statusEl) statusEl.textContent = 'Saving...';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${apiBase}/api/communities/${community.id}/plugins/${pluginName}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'Authorization': `Bearer ${token}`,
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ settings }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
if (statusEl) statusEl.textContent = 'Saved';
|
|
|
|
|
setTimeout(() => { if (statusEl) statusEl.textContent = ''; }, 1500);
|
|
|
|
|
} else {
|
|
|
|
|
const err = await res.text();
|
|
|
|
|
if (statusEl) statusEl.textContent = 'Error';
|
|
|
|
|
alert(err || 'Failed to save settings');
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (statusEl) statusEl.textContent = 'Error';
|
|
|
|
|
alert('Failed to save settings');
|
|
|
|
|
} finally {
|
|
|
|
|
if (btn) btn.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function load() {
|
|
|
|
|
const container = document.getElementById('plugins-content');
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const community = await fetchCommunityBySlug();
|
|
|
|
|
if (!community) {
|
|
|
|
|
container.innerHTML = '<div class="error">Community not found</div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const membership = await fetchMembership(community.id);
|
|
|
|
|
const plugins = await fetchPlugins(community.id);
|
|
|
|
|
|
|
|
|
|
renderPlugins(community, membership, plugins);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
container.innerHTML = `<div class="error">${escapeHtml(err?.message || 'Failed to load')}</div>`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
load();
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style>
|
2026-01-29 16:52:30 +00:00
|
|
|
.hero {
|
|
|
|
|
padding: 1.25rem;
|
|
|
|
|
margin-bottom: 1rem;
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 16:52:30 +00:00
|
|
|
.hero-top {
|
2026-01-27 16:21:58 +00:00
|
|
|
display: flex;
|
|
|
|
|
align-items: flex-start;
|
2026-01-29 16:52:30 +00:00
|
|
|
justify-content: space-between;
|
2026-01-27 16:21:58 +00:00
|
|
|
gap: 1rem;
|
2026-01-29 16:52:30 +00:00
|
|
|
flex-wrap: wrap;
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 16:52:30 +00:00
|
|
|
.hero-title {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 2.125rem;
|
|
|
|
|
letter-spacing: -0.02em;
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 16:52:30 +00:00
|
|
|
.hero-subtitle {
|
|
|
|
|
margin: 0.25rem 0 0;
|
2026-01-27 16:21:58 +00:00
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.community-badge {
|
2026-01-29 16:52:30 +00:00
|
|
|
padding: 1rem 1.1rem;
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.community-badge-title {
|
2026-01-27 16:21:58 +00:00
|
|
|
display: flex;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
align-items: baseline;
|
2026-01-29 16:52:30 +00:00
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.community-badge-subtitle {
|
|
|
|
|
margin-top: 0.35rem;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
font-size: 0.875rem;
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.community-badge .name {
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.community-badge .slug {
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plugins-list {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plugin-card {
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plugin-head {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
margin-bottom: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plugin-title {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plugin-title h3 {
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.version {
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 16:52:30 +00:00
|
|
|
.tag-core {
|
|
|
|
|
background: var(--color-info-muted);
|
2026-01-27 16:21:58 +00:00
|
|
|
color: var(--color-info);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 16:52:30 +00:00
|
|
|
.tag-disabled {
|
|
|
|
|
background: var(--color-error-muted);
|
2026-01-27 16:21:58 +00:00
|
|
|
color: var(--color-error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.desc {
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
margin-bottom: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 16:52:30 +00:00
|
|
|
.settings-panel {
|
|
|
|
|
margin-top: 0.75rem;
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.settings-body {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
textarea.settings-json {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-height: 140px;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
background: var(--color-field-bg);
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
resize: vertical;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.settings-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.75rem;
|
2026-01-29 16:52:30 +00:00
|
|
|
flex-wrap: wrap;
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status {
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.switch {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 44px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.switch input {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
width: 0;
|
|
|
|
|
height: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slider {
|
|
|
|
|
position: absolute;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
background-color: var(--color-neutral);
|
|
|
|
|
transition: 0.2s;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slider:before {
|
|
|
|
|
position: absolute;
|
|
|
|
|
content: '';
|
|
|
|
|
height: 18px;
|
|
|
|
|
width: 18px;
|
|
|
|
|
left: 3px;
|
|
|
|
|
bottom: 3px;
|
|
|
|
|
background-color: var(--color-on-primary);
|
|
|
|
|
transition: 0.2s;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
input:checked + .slider {
|
|
|
|
|
background-color: var(--color-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
input:checked + .slider:before {
|
|
|
|
|
transform: translateX(20px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
input:disabled + .slider {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
cursor: default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.error {
|
|
|
|
|
background: var(--color-error-muted);
|
|
|
|
|
border: 1px solid var(--color-error);
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.settings-body-wrap {
|
|
|
|
|
padding-top: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.settings-form {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.schema-field {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.schema-field label {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.schema-field input[type="text"],
|
|
|
|
|
.schema-field input[type="number"],
|
|
|
|
|
.schema-field select {
|
2026-01-29 16:52:30 +00:00
|
|
|
border-radius: var(--radius-md);
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.schema-field input[type="text"]:focus,
|
|
|
|
|
.schema-field input[type="number"]:focus,
|
|
|
|
|
.schema-field select:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
border-color: var(--color-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.checkbox-label {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.checkbox-label input[type="checkbox"] {
|
|
|
|
|
width: 18px;
|
|
|
|
|
height: 18px;
|
|
|
|
|
accent-color: var(--color-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.field-hint {
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.settings-hint {
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 16:52:30 +00:00
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.plugin-head {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
</style>
|