Split settings into separate sub-pages with sidebar nav tree
Each settings nav item (General, Connections, Playback, Payloads, Storage, API Keys, Backup, Logs, About) now shows its own isolated card page. navSettingsCat() toggles .s-settings-page visibility + active state. Save/Reload use event delegation (.s-save-btn/.s-reload-btn classes). Default sub-page is Connections on first entry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cd770801dc
commit
a72e49b807
@ -1970,12 +1970,15 @@ document.querySelectorAll('.s-eye-btn').forEach(btn => {
|
||||
});
|
||||
});
|
||||
$('settings-btn')?.addEventListener('click', async () => { await loadSettings(); openSettings(false); });
|
||||
$('s-close-btn').addEventListener('click', async () => { await loadSettings(); toast('Settings reloaded', 'success'); });
|
||||
document.addEventListener('click', async e => {
|
||||
if (e.target.closest('.s-reload-btn')) { await loadSettings(); toast('Settings reloaded', 'success'); }
|
||||
});
|
||||
$('s-use-parakeet-asr')?.addEventListener('click', () => { $('s-whisper-url').value = $('s-nvidia-asr-url').value || 'http://host.docker.internal:8092'; });
|
||||
$('s-use-nvidia-router')?.addEventListener('click', () => { const url = $('s-nvidia-router-url').value || 'http://host.docker.internal:8090'; $('s-whisper-url').value = url; $('s-nvidia-tts-url').value = url; });
|
||||
$('s-use-faster-whisper')?.addEventListener('click', () => { $('s-whisper-url').value = $('s-faster-whisper-url').value || 'http://host.docker.internal:8000'; });
|
||||
$('s-use-whisper-cpp')?.addEventListener('click', () => { $('s-whisper-url').value = $('s-whisper-cpp-url').value || 'http://host.docker.internal:8080'; });
|
||||
$('s-save-btn').addEventListener('click', async () => {
|
||||
document.addEventListener('click', async e => { if (!e.target.closest('.s-save-btn')) return;
|
||||
{
|
||||
let ttsExtraParamsByBackend = {};
|
||||
const paramFields = [
|
||||
['voice_clone', 's-tts-extra-voice-clone', 'Voice Clone/Base'],
|
||||
@ -2036,7 +2039,7 @@ $('s-save-btn').addEventListener('click', async () => {
|
||||
markSettingsSeen();
|
||||
renderIntegrationSnippets();
|
||||
toast('Settings saved', 'success');
|
||||
});
|
||||
}});
|
||||
|
||||
// Theme select in General settings
|
||||
document.addEventListener('change', e => {
|
||||
|
||||
@ -29,6 +29,15 @@
|
||||
if (name === 'getvoices' && typeof loadGetVoices === 'function') loadGetVoices();
|
||||
}
|
||||
|
||||
function applySettingsPage(cat) {
|
||||
document.querySelectorAll('.s-settings-page').forEach(function (p) {
|
||||
p.classList.toggle('is-active', p.dataset.page === cat);
|
||||
});
|
||||
document.querySelectorAll('[data-settings-cat]').forEach(function (el) {
|
||||
el.classList.toggle('is-active', el.dataset.settingsCat === cat);
|
||||
});
|
||||
}
|
||||
|
||||
function showSection(sectionId) {
|
||||
SECTIONS.forEach(function (id) {
|
||||
var el = document.getElementById(id);
|
||||
@ -41,16 +50,19 @@
|
||||
// Voices nav tree
|
||||
var voicesTree = document.getElementById('nav-voices-tree');
|
||||
var voicesChevron = document.getElementById('nav-voices-chevron');
|
||||
var isVoices = sectionId === 's-voices';
|
||||
if (voicesTree) voicesTree.classList.toggle('open', isVoices);
|
||||
if (voicesChevron) voicesChevron.classList.toggle('open', isVoices);
|
||||
if (voicesTree) voicesTree.classList.toggle('open', sectionId === 's-voices');
|
||||
if (voicesChevron) voicesChevron.classList.toggle('open', sectionId === 's-voices');
|
||||
|
||||
// Settings nav tree
|
||||
var settingsTree = document.getElementById('nav-settings-tree');
|
||||
var settingsChevron = document.getElementById('nav-settings-chevron');
|
||||
var isSettings = sectionId === 's-settings';
|
||||
if (settingsTree) settingsTree.classList.toggle('open', isSettings);
|
||||
if (settingsChevron) settingsChevron.classList.toggle('open', isSettings);
|
||||
if (settingsTree) settingsTree.classList.toggle('open', sectionId === 's-settings');
|
||||
if (settingsChevron) settingsChevron.classList.toggle('open', sectionId === 's-settings');
|
||||
|
||||
// When entering settings, show the active sub-page (default: connections)
|
||||
if (sectionId === 's-settings') {
|
||||
applySettingsPage(window._settingsSidebarCat || 'connections');
|
||||
}
|
||||
|
||||
var main = document.getElementById('main-content');
|
||||
if (main) main.scrollTop = 0;
|
||||
@ -96,27 +108,13 @@
|
||||
};
|
||||
|
||||
// ── Settings tree sub-navigation ──────────────────────────────────────────
|
||||
window._settingsSidebarCat = null;
|
||||
window._settingsSidebarCat = 'connections';
|
||||
|
||||
window.navSettingsCat = function (cat) {
|
||||
navTo('s-settings');
|
||||
window._settingsSidebarCat = cat;
|
||||
document.querySelectorAll('[data-settings-cat]').forEach(function (el) {
|
||||
el.classList.toggle('is-active', el.dataset.settingsCat === cat);
|
||||
});
|
||||
navTo('s-settings'); // showSection will call applySettingsPage(cat)
|
||||
if (cat === 'logs' && typeof loadSettingsLogs === 'function') loadSettingsLogs();
|
||||
// Scroll inside main-content to the target section
|
||||
var target = document.getElementById('s-settings-' + cat);
|
||||
if (target) {
|
||||
setTimeout(function () {
|
||||
var main = document.getElementById('main-content');
|
||||
if (main) {
|
||||
var mainRect = main.getBoundingClientRect();
|
||||
var targetRect = target.getBoundingClientRect();
|
||||
main.scrollTo({ top: main.scrollTop + targetRect.top - mainRect.top - 16, behavior: 'smooth' });
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
if (cat === 'about' && typeof renderSettingsAbout === 'function') renderSettingsAbout();
|
||||
};
|
||||
|
||||
// Show default section on load
|
||||
|
||||
@ -7,20 +7,14 @@
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-settings">
|
||||
<div class="card" id="settings-box">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="s-header">
|
||||
<div>
|
||||
<h2>Settings</h2>
|
||||
<p class="s-subtitle">Configure the service URLs you use. Advanced payloads, folders, and keys are below.</p>
|
||||
<!-- ── General ─────────────────────────────────────────────── -->
|
||||
<div class="s-settings-page" data-page="general">
|
||||
<div class="card">
|
||||
<div class="s-page-head">
|
||||
<h3>General</h3>
|
||||
<p>Interface preferences.</p>
|
||||
</div>
|
||||
<span class="s-stack-badge"><span class="mdi mdi-check-circle-outline"></span> Local stack</span>
|
||||
</div>
|
||||
|
||||
<!-- General -->
|
||||
<div class="s-section" id="s-settings-general">
|
||||
<div class="s-section-label">General</div>
|
||||
<div class="settings-grid settings-behavior-grid">
|
||||
<div class="s-field">
|
||||
<label>Theme</label>
|
||||
@ -28,15 +22,24 @@
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
<span class="s-hint">Switch between light and dark interface.</span>
|
||||
<span class="s-hint">Switch between light and dark interface. Saved instantly.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Core connections -->
|
||||
<div class="s-section" id="s-settings-connections">
|
||||
<div class="s-section-label">Connections</div>
|
||||
<p class="s-section-desc">Service URLs for each backend. Change these first when setting up.</p>
|
||||
<!-- ── Connections ─────────────────────────────────────────── -->
|
||||
<div class="s-settings-page" data-page="connections">
|
||||
<div class="card">
|
||||
<div class="s-page-head">
|
||||
<div class="s-page-head-row">
|
||||
<div>
|
||||
<h3>Connections</h3>
|
||||
<p>Service URLs for each backend. Change these first when setting up.</p>
|
||||
</div>
|
||||
<span class="s-stack-badge"><span class="mdi mdi-check-circle-outline"></span> Local stack</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="s-group">
|
||||
<div class="s-group-head">
|
||||
@ -139,16 +142,25 @@
|
||||
<input type="password" id="s-groq-api-key" placeholder="gsk_…" autocomplete="off">
|
||||
<button type="button" class="s-eye-btn" data-target="s-groq-api-key"><span class="mdi mdi-eye-outline"></span></button>
|
||||
</div>
|
||||
<span class="s-hint">Used for Groq Whisper STT and LLM. Free: 2 000 req/day. Fastest cloud transcription.</span>
|
||||
<span class="s-hint">Used for Groq Whisper STT and LLM. Free: 2 000 req/day.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Playback behavior -->
|
||||
<div class="s-section" id="s-settings-playback">
|
||||
<div class="s-section-label">Playback</div>
|
||||
<p class="s-section-desc">Controls how previews play and how OpenAI-compatible requests are shaped.</p>
|
||||
<div class="s-page-actions">
|
||||
<button class="btn-primary s-save-btn">Save settings</button>
|
||||
<button class="btn-secondary s-reload-btn">Reload settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Playback ─────────────────────────────────────────────── -->
|
||||
<div class="s-settings-page" data-page="playback">
|
||||
<div class="card">
|
||||
<div class="s-page-head">
|
||||
<h3>Playback</h3>
|
||||
<p>Controls how previews play and how OpenAI-compatible requests are shaped.</p>
|
||||
</div>
|
||||
<div class="settings-grid settings-behavior-grid">
|
||||
<div class="s-field">
|
||||
<label>TTS preview playback</label>
|
||||
@ -170,23 +182,21 @@
|
||||
<span class="s-hint">Controls payload shape for the normal TTS API URL.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="s-page-actions">
|
||||
<button class="btn-primary s-save-btn">Save settings</button>
|
||||
<button class="btn-secondary s-reload-btn">Reload settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save / Reload -->
|
||||
<div class="btn-row settings-actions">
|
||||
<button class="btn-primary" id="s-save-btn">Save settings</button>
|
||||
<button class="btn-secondary" id="s-close-btn">Reload settings</button>
|
||||
</div>
|
||||
|
||||
<!-- Advanced payloads -->
|
||||
<details class="s-details" id="s-settings-payloads">
|
||||
<summary>
|
||||
<span class="mdi mdi-code-braces"></span>
|
||||
<span class="s-details-title">Payloads</span>
|
||||
<small>JSON extras sent to each backend</small>
|
||||
<span class="mdi mdi-chevron-down s-details-chevron"></span>
|
||||
</summary>
|
||||
<div class="settings-grid three settings-param-grid s-details-body">
|
||||
<!-- ── Payloads ─────────────────────────────────────────────── -->
|
||||
<div class="s-settings-page" data-page="payloads">
|
||||
<div class="card">
|
||||
<div class="s-page-head">
|
||||
<h3>Payloads</h3>
|
||||
<p>Extra JSON fields sent alongside each backend request. Usually only needed for advanced tuning.</p>
|
||||
</div>
|
||||
<div class="settings-grid three settings-param-grid">
|
||||
<div class="s-field">
|
||||
<label>Voice Clone / Base params</label>
|
||||
<textarea id="s-tts-extra-voice-clone" spellcheck="false" placeholder='{"temperature":0.1,"top_p":0.8,"seed":0}'></textarea>
|
||||
@ -228,17 +238,21 @@
|
||||
<span class="s-hint">Extra JSON fields for Kokoro. Usually empty — voice is selected from the dropdown.</span>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div class="s-page-actions">
|
||||
<button class="btn-primary s-save-btn">Save settings</button>
|
||||
<button class="btn-secondary s-reload-btn">Reload settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Voice folders -->
|
||||
<details class="s-details" id="s-settings-storage">
|
||||
<summary>
|
||||
<span class="mdi mdi-folder-outline"></span>
|
||||
<span class="s-details-title">Storage</span>
|
||||
<small>container paths and Portainer volume mounts</small>
|
||||
<span class="mdi mdi-chevron-down s-details-chevron"></span>
|
||||
</summary>
|
||||
<div class="settings-grid s-details-body">
|
||||
<!-- ── Storage ─────────────────────────────────────────────── -->
|
||||
<div class="s-settings-page" data-page="storage">
|
||||
<div class="card">
|
||||
<div class="s-page-head">
|
||||
<h3>Storage</h3>
|
||||
<p>Container paths for voice files. Match these to your Portainer volume mounts.</p>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="s-field">
|
||||
<label>Voice scan directory</label>
|
||||
<input type="text" id="s-voices-scan-dir" placeholder="/voices">
|
||||
@ -250,17 +264,21 @@
|
||||
<span class="s-hint">New cloned/exported voices are saved here.</span>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div class="s-page-actions">
|
||||
<button class="btn-primary s-save-btn">Save settings</button>
|
||||
<button class="btn-secondary s-reload-btn">Reload settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API keys -->
|
||||
<details class="s-details" id="s-settings-apikeys">
|
||||
<summary>
|
||||
<span class="mdi mdi-key-outline"></span>
|
||||
<span class="s-details-title">API Keys</span>
|
||||
<small>usually empty for local containers</small>
|
||||
<span class="mdi mdi-chevron-down s-details-chevron"></span>
|
||||
</summary>
|
||||
<div class="settings-grid three s-details-body">
|
||||
<!-- ── API Keys ─────────────────────────────────────────────── -->
|
||||
<div class="s-settings-page" data-page="apikeys">
|
||||
<div class="card">
|
||||
<div class="s-page-head">
|
||||
<h3>API Keys</h3>
|
||||
<p>Usually empty for local containers. Set a dummy value like <code>sk-local</code> if a backend requires an Authorization header.</p>
|
||||
</div>
|
||||
<div class="settings-grid three">
|
||||
<div class="s-field">
|
||||
<label>TTS API key <span class="s-label-note">(optional)</span></label>
|
||||
<div class="s-key-row">
|
||||
@ -283,13 +301,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div class="s-page-actions">
|
||||
<button class="btn-primary s-save-btn">Save settings</button>
|
||||
<button class="btn-secondary s-reload-btn">Reload settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backup & restore -->
|
||||
<div class="s-section" id="s-settings-backup">
|
||||
<div class="s-section-label">Backup & restore</div>
|
||||
<p class="s-hint">Export all voices and settings as a ZIP for backup or migration. Import to restore. API keys are excluded from exports.</p>
|
||||
<div class="btn-row" style="margin-top:8px;gap:10px;flex-wrap:wrap">
|
||||
<!-- ── Backup ─────────────────────────────────────────────── -->
|
||||
<div class="s-settings-page" data-page="backup">
|
||||
<div class="card">
|
||||
<div class="s-page-head">
|
||||
<h3>Backup & restore</h3>
|
||||
<p>Export all voices and settings as a ZIP for backup or migration. API keys are excluded from exports.</p>
|
||||
</div>
|
||||
<div class="btn-row" style="gap:10px;flex-wrap:wrap">
|
||||
<a class="btn-secondary" id="s-export-voices-btn" href="/api/voices/export" download><span class="mdi mdi-download"></span> Export voices</a>
|
||||
<label class="btn-secondary" style="cursor:pointer" title="Import a voices ZIP">
|
||||
<span class="mdi mdi-upload"></span> Import voices
|
||||
@ -298,11 +324,15 @@
|
||||
<span class="note" id="s-import-status"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="s-section" id="s-settings-logs">
|
||||
<div class="s-section-label">Logs</div>
|
||||
<p class="s-section-desc">Recent server activity. Useful for debugging backend connections and API errors.</p>
|
||||
<!-- ── Logs ─────────────────────────────────────────────────── -->
|
||||
<div class="s-settings-page" data-page="logs">
|
||||
<div class="card">
|
||||
<div class="s-page-head">
|
||||
<h3>Logs</h3>
|
||||
<p>Recent server activity. Useful for debugging backend connections and API errors.</p>
|
||||
</div>
|
||||
<div class="s-log-toolbar">
|
||||
<button class="btn-secondary" id="s-logs-refresh-btn" type="button"><span class="mdi mdi-refresh"></span> Refresh</button>
|
||||
<button class="btn-secondary" id="s-logs-clear-btn" type="button"><span class="mdi mdi-trash-can-outline"></span> Clear</button>
|
||||
@ -322,10 +352,14 @@
|
||||
<div class="s-log-empty">Click Refresh to load logs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- About -->
|
||||
<div class="s-section s-section-last" id="s-settings-about">
|
||||
<div class="s-section-label">About</div>
|
||||
<!-- ── About ─────────────────────────────────────────────────── -->
|
||||
<div class="s-settings-page" data-page="about">
|
||||
<div class="card">
|
||||
<div class="s-page-head">
|
||||
<h3>About</h3>
|
||||
</div>
|
||||
<div class="s-about-block">
|
||||
<div class="s-about-name"><span class="mdi mdi-microphone-variant"></span> TTS Voice Creator</div>
|
||||
<p class="s-about-desc">Clone, design, and deploy custom voices using local AI backends. Compatible with Qwen3-TTS, Kokoro FastAPI, NVIDIA Magpie, and any OpenAI-compatible TTS/STT endpoint.</p>
|
||||
@ -338,6 +372,6 @@
|
||||
<div class="s-about-backends" id="s-about-backends"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-settings -->
|
||||
|
||||
@ -317,24 +317,20 @@ audio { width: 100%; }
|
||||
.preview-match-transcript { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px; color: var(--subtext); font-family: monospace; font-size: 12px; line-height: 1.4; max-height: 88px; overflow: auto; white-space: pre-wrap; }
|
||||
@media (max-width: 900px) { .preview-match-panel { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ── Settings ───────────────────────────────────────────────────────────── */
|
||||
#settings-box { display:flex; flex-direction:column; gap:0; width:100%; max-width:none; box-sizing:border-box; padding:0; overflow:hidden; }
|
||||
/* ── Settings pages ─────────────────────────────────────────────────────── */
|
||||
.s-settings-page { display:none; flex-direction:column; gap:16px; }
|
||||
.s-settings-page.is-active { display:flex; }
|
||||
|
||||
/* Header */
|
||||
.s-header { display:flex; justify-content:space-between; align-items:flex-start; gap:20px; padding:28px 32px 24px; border-bottom:1px solid var(--border); }
|
||||
.s-header h2 { font-size:18px; font-weight:700; margin:0 0 5px; }
|
||||
.s-subtitle { font-size:13px; color:var(--subtext); line-height:1.55; margin:0; max-width:580px; }
|
||||
.s-stack-badge { display:inline-flex; align-items:center; gap:5px; border:1px solid color-mix(in srgb,var(--green) 40%,transparent); background:rgba(22,163,74,.07); color:var(--green); border-radius:999px; padding:5px 11px; font-size:12px; font-weight:600; white-space:nowrap; flex-shrink:0; margin-top:2px; }
|
||||
/* Page card head */
|
||||
.s-page-head { border-bottom:1px solid var(--border); padding-bottom:14px; }
|
||||
.s-page-head h3 { font-size:16px; font-weight:700; margin:0 0 4px; color:var(--text); }
|
||||
.s-page-head p { font-size:13px; color:var(--subtext); margin:0; line-height:1.5; }
|
||||
.s-page-head-row { display:flex; justify-content:space-between; align-items:flex-start; gap:16px; }
|
||||
.s-stack-badge { display:inline-flex; align-items:center; gap:5px; border:1px solid color-mix(in srgb,var(--green) 40%,transparent); background:rgba(22,163,74,.07); color:var(--green); border-radius:999px; padding:5px 11px; font-size:12px; font-weight:600; white-space:nowrap; flex-shrink:0; }
|
||||
|
||||
/* Sections */
|
||||
.s-section { padding:26px 32px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:22px; }
|
||||
.s-section-last { border-bottom:none; }
|
||||
.s-section-label { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.1em; color:var(--subtext); }
|
||||
.s-section-desc { font-size:13px; color:var(--subtext); margin:0; line-height:1.5; }
|
||||
|
||||
/* Field groups within a section */
|
||||
.s-group { display:flex; flex-direction:column; gap:14px; padding-top:4px; }
|
||||
.s-group + .s-group { border-top:1px solid var(--border); padding-top:22px; }
|
||||
/* Groups */
|
||||
.s-group { display:flex; flex-direction:column; gap:14px; }
|
||||
.s-group + .s-group { border-top:1px solid var(--border); padding-top:18px; }
|
||||
.s-group-head { display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; }
|
||||
.s-group-head strong { font-size:14px; font-weight:600; color:var(--text); }
|
||||
.s-group-head span { font-size:12px; color:var(--subtext); }
|
||||
@ -344,8 +340,9 @@ audio { width: 100%; }
|
||||
.settings-grid.three { grid-template-columns:repeat(auto-fit, minmax(240px,1fr)); }
|
||||
.settings-grid.compact { grid-template-columns:repeat(2, minmax(220px,1fr)); gap:14px; }
|
||||
.settings-behavior-grid { grid-template-columns:minmax(220px,.6fr) minmax(220px,.6fr); gap:16px; align-items:start; }
|
||||
.settings-param-grid textarea { min-height:58px; }
|
||||
|
||||
/* Individual field */
|
||||
/* Fields */
|
||||
.s-field { display:flex; flex-direction:column; gap:6px; }
|
||||
.s-field label { font-size:13px; color:var(--text); font-weight:500; }
|
||||
.s-label-note { font-weight:400; color:var(--subtext); }
|
||||
@ -363,21 +360,9 @@ audio { width: 100%; }
|
||||
.settings-mini-actions { margin-top:4px; gap:8px; flex-wrap:wrap; }
|
||||
.settings-mini-actions button { min-height:32px; padding:6px 11px; font-size:12px; }
|
||||
|
||||
/* Expandable details */
|
||||
.s-details { border-bottom:1px solid var(--border); }
|
||||
.s-details summary { list-style:none; cursor:pointer; padding:16px 32px; display:flex; align-items:center; gap:10px; color:var(--text); font-size:14px; font-weight:600; user-select:none; transition:background .15s; }
|
||||
.s-details summary:hover { background:rgba(0,0,0,.02); }
|
||||
.s-details summary::-webkit-details-marker { display:none; }
|
||||
.s-details-title { flex:1; }
|
||||
.s-details summary small { color:var(--subtext); font-weight:400; font-size:13px; }
|
||||
.s-details-chevron { color:var(--subtext); font-size:18px; transition:transform .2s; flex-shrink:0; }
|
||||
.s-details[open] .s-details-chevron { transform:rotate(180deg); }
|
||||
.s-details-body { padding:4px 32px 24px; }
|
||||
.settings-param-grid textarea { min-height:58px; }
|
||||
|
||||
/* Actions row */
|
||||
.settings-actions { display:flex; gap:10px; align-items:center; padding:20px 32px; border-top:1px solid var(--border); border-bottom:1px solid var(--border); }
|
||||
.settings-actions button { min-width:128px; }
|
||||
/* Page save/reload actions */
|
||||
.s-page-actions { border-top:1px solid var(--border); padding-top:14px; display:flex; gap:10px; }
|
||||
.s-page-actions button { min-width:120px; }
|
||||
|
||||
/* Log viewer */
|
||||
.s-log-toolbar { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
||||
@ -388,7 +373,7 @@ audio { width: 100%; }
|
||||
.s-log-filter { border:1px solid var(--border); background:transparent; color:var(--subtext); border-radius:999px; padding:3px 10px; font-size:12px; cursor:pointer; transition:all .15s; }
|
||||
.s-log-filter:hover { border-color:var(--accent); color:var(--accent); }
|
||||
.s-log-filter.is-active { background:var(--accent); border-color:var(--accent); color:#fff; }
|
||||
.s-log-viewer { background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); padding:12px 14px; font-family:monospace; font-size:12px; line-height:1.6; max-height:380px; overflow-y:auto; display:flex; flex-direction:column; gap:1px; }
|
||||
.s-log-viewer { background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); padding:12px 14px; font-family:monospace; font-size:12px; line-height:1.6; max-height:440px; overflow-y:auto; display:flex; flex-direction:column; gap:1px; }
|
||||
.s-log-empty { color:var(--subtext); font-family:inherit; text-align:center; padding:24px 0; }
|
||||
.s-log-row { display:grid; grid-template-columns:130px 56px 1fr; gap:8px; align-items:baseline; padding:2px 0; border-bottom:1px solid color-mix(in srgb,var(--border) 50%,transparent); }
|
||||
.s-log-row:last-child { border-bottom:none; }
|
||||
@ -407,7 +392,7 @@ audio { width: 100%; }
|
||||
.s-about-desc { font-size:14px; color:var(--subtext); line-height:1.65; margin:0; }
|
||||
.s-about-stack { display:flex; flex-wrap:wrap; gap:8px; }
|
||||
.s-about-chip { display:inline-flex; align-items:center; gap:5px; background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); padding:4px 10px; font-size:12px; color:var(--subtext); }
|
||||
.s-about-backends { display:flex; flex-wrap:wrap; gap:8px; }
|
||||
.s-about-backends { display:flex; flex-wrap:wrap; gap:8px; margin-top:4px; }
|
||||
.s-about-backend { display:inline-flex; align-items:center; gap:6px; border:1px solid var(--border); border-radius:var(--radius); padding:5px 10px; font-size:12px; color:var(--subtext); }
|
||||
.s-about-backend.online { border-color:color-mix(in srgb,var(--green) 50%,transparent); color:var(--green); }
|
||||
.s-about-backend.offline { opacity:.55; }
|
||||
@ -415,12 +400,7 @@ audio { width: 100%; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width:1100px) { .settings-grid.compact { grid-template-columns:1fr; } .settings-behavior-grid { grid-template-columns:1fr; } }
|
||||
@media (max-width:900px) {
|
||||
.stt-settings-grid { grid-template-columns:1fr; }
|
||||
.s-header, .s-section, .settings-actions { padding-left:18px; padding-right:18px; }
|
||||
.s-details summary { padding-left:18px; padding-right:18px; }
|
||||
.s-details-body { padding-left:18px; padding-right:18px; }
|
||||
}
|
||||
@media (max-width:900px) { .stt-settings-grid { grid-template-columns:1fr; } }
|
||||
|
||||
/* ── Toast ──────────────────────────────────────────────────────────────── */
|
||||
#toast { position: fixed; bottom: 24px; right: 24px; background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 14px 20px; border-radius: var(--radius); font-size: 14px; opacity: 0; transition: opacity .3s; pointer-events: none; z-index: 200; max-width: 380px; box-shadow: var(--shadow); }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user