Tags: comma-separated + DB autocomplete; collapsible opt-groups; reorder voice list pane

Tags:
- Comma-separated input (tag1, tag2, tag3) with placeholder updated
- Custom autocomplete dropdown on last token, sources from _voices DB + localStorage
- All comma-separated tokens saved individually to localStorage reuse list

Inspector opt-groups:
- Each group wrapped in opt-group-body for clean collapse
- Chevron (▾/▸) in title, click title to toggle open/closed
- Transcript and Loudness open by default; others collapsed
- Loudness title preserves dBFS meta span + chevron

Voice list pane reorder:
- vl-synth-panel moved above vl-filters
- vl-filters now sits directly above voice-list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-05-26 12:02:44 +02:00
parent 28f5ec2e25
commit ecef913837
3 changed files with 219 additions and 123 deletions

View File

@ -116,7 +116,7 @@ function selectVoice(wrap) {
<span class="insp-stars">${makeStars(rating)}</span>
<span class="insp-rating-label">${rating}/5</span>
</span>
<input class="insp-tag-input" type="text" placeholder="add tag…" value="${escHtml(v.tag||'')}" list="insp-tag-opts" autocomplete="off">
<input class="insp-tag-input" type="text" placeholder="tag1, tag2…" value="${escHtml(v.tag||'')}" autocomplete="off">
</div>
<div class="insp-note-delete-row">
<div class="insp-note-slot"></div>
@ -249,20 +249,71 @@ function selectVoice(wrap) {
});
genderSel?.addEventListener('change', () => applyGender(genderSel.value));
// ── Tag input with reuse list ─────────────────────────────────────────────
// ── Tag input — comma-separated, autocomplete from DB + localStorage ────────
const tagInput = inspector.querySelector('.insp-tag-input');
let _tagDl = document.getElementById('insp-tag-opts');
if (!_tagDl) { _tagDl = document.createElement('datalist'); _tagDl.id = 'insp-tag-opts'; document.body.appendChild(_tagDl); }
_tagDl.innerHTML = getStoredTags().map(t => `<option value="${escHtml(t)}">`).join('');
tagInput.addEventListener('change', () => {
const t = tagInput.value.trim();
if (t) { addStoredTag(t); _tagDl.innerHTML = getStoredTags().map(x => `<option value="${escHtml(x)}">`).join(''); }
const getAllKnownTags = () => {
const fromDb = (_voices || []).flatMap(vv =>
(vv.tag || '').split(',').map(t => t.trim()).filter(Boolean));
const fromStorage = getStoredTags();
return [...new Set([...fromStorage, ...fromDb])].sort((a, b) => a.localeCompare(b));
};
const tagLastToken = val => val.split(',').pop().trimStart();
const tagReplaceLastToken = (val, rep) => {
const parts = val.split(',');
parts[parts.length - 1] = parts.length > 1 ? ' ' + rep : rep;
return parts.join(',');
};
let _tagDrop = null;
const hideTagDrop = () => { _tagDrop?.remove(); _tagDrop = null; };
const showTagDrop = () => {
hideTagDrop();
const token = tagLastToken(tagInput.value);
const all = getAllKnownTags();
const matches = all.filter(t =>
t.toLowerCase().startsWith(token.toLowerCase()) && t.toLowerCase() !== token.toLowerCase()
);
if (!matches.length) return;
_tagDrop = document.createElement('div');
_tagDrop.className = 'tag-suggest';
matches.slice(0, 10).forEach(tag => {
const btn = document.createElement('button');
btn.type = 'button'; btn.className = 'tag-suggest-item';
btn.textContent = tag;
btn.addEventListener('mousedown', e => {
e.preventDefault();
tagInput.value = tagReplaceLastToken(tagInput.value, tag);
tagInput.dispatchEvent(new Event('input'));
hideTagDrop();
tagInput.focus();
});
tagInput.addEventListener('input', debounce(async () => {
_tagDrop.appendChild(btn);
});
document.body.appendChild(_tagDrop);
const r = tagInput.getBoundingClientRect();
_tagDrop.style.top = (r.bottom + 3) + 'px';
_tagDrop.style.left = r.left + 'px';
_tagDrop.style.minWidth = Math.max(r.width, 140) + 'px';
};
tagInput.addEventListener('input', () => showTagDrop());
tagInput.addEventListener('focus', () => showTagDrop());
tagInput.addEventListener('blur', () => setTimeout(hideTagDrop, 150));
tagInput.addEventListener('keydown', e => {
if (e.key === 'Escape') hideTagDrop();
if (e.key === ',' && _tagDrop) setTimeout(showTagDrop, 10);
});
const saveTagValue = debounce(async () => {
const tags = tagInput.value.split(',').map(t => t.trim()).filter(Boolean);
tags.forEach(addStoredTag);
v.tag = tagInput.value;
await saveMeta(voiceId, { tag: tagInput.value });
}, 800));
}, 700);
tagInput.addEventListener('input', saveTagValue);
tagInput.addEventListener('change', saveTagValue);
// ── Interactive rating (subtitle + meta kept in sync) ─────────────────────
const updateRating = async (newRating) => {
@ -306,9 +357,16 @@ function selectVoice(wrap) {
const maintTitle = body.querySelector('.opt-maintenance .opt-group-title');
if (maintTitle) {
maintTitle.innerHTML = `Loudness <span class="opt-group-meta">Current ${escHtml(dbfs)} dBFS</span>`;
maintTitle.innerHTML = `Loudness <span class="opt-group-meta">Current ${escHtml(dbfs)} dBFS</span> <span class="opt-chevron">&#8964;</span>`;
}
// ── Collapsible opt-groups ────────────────────────────────────────────────
body.querySelectorAll('.opt-group').forEach(group => {
const title = group.querySelector(':scope > .opt-group-title');
if (!title) return;
title.addEventListener('click', () => group.classList.toggle('open'));
});
// Track extracted elements for restoreToRow
wrap._extracted = [
activeEl ? {el: activeEl, target: detailRow} : null,
@ -4324,7 +4382,8 @@ function makeVoiceRow(v) {
<div class="vr-optimizer">
<div class="optimizer-grid">
<div class="opt-group opt-trim-panel">
<div class="opt-group-title">Reference audio &middot; crop</div>
<div class="opt-group-title">Reference audio &middot; crop <span class="opt-chevron">&#8964;</span></div>
<div class="opt-group-body">
<canvas class="opt-wave"></canvas>
<div class="opt-controls">
<div class="opt-field"><label>Start</label><input class="opt-start" type="number" step="0.01" value="0"></div>
@ -4335,16 +4394,20 @@ function makeVoiceRow(v) {
<button class="btn-secondary opt-undo">Undo crop</button>
</div>
</div>
<div class="opt-group opt-text-panel">
<div class="opt-group-title">Reference transcript</div>
</div>
<div class="opt-group opt-text-panel open">
<div class="opt-group-title">Reference transcript <span class="opt-chevron">&#8964;</span></div>
<div class="opt-group-body">
<textarea class="opt-transcript" placeholder="Reference text">${escHtml(v.transcript || '')}</textarea>
<div class="opt-controls">
<button class="btn-secondary opt-recognize">Re-recognise text</button>
<button class="btn-primary opt-save-text">Save text</button>
</div>
</div>
</div>
<div class="opt-group opt-compare-panel">
<div class="opt-group-title">Voice match</div>
<div class="opt-group-title">Voice match <span class="opt-chevron">&#8964;</span></div>
<div class="opt-group-body">
<div class="opt-controls">
<div class="opt-field wide"><label>Comparison backend</label><select class="opt-compare-backend">${styleBackendOptions('voice_clone')}</select></div>
<button class="btn-secondary opt-play-reference">Play WAV file</button>
@ -4362,9 +4425,10 @@ function makeVoiceRow(v) {
</div>
</div>
</div>
</div>
<div class="opt-group opt-style-panel">
<div>
<div class="opt-group-title">Style variation</div>
<div class="opt-group-title">Style variation <span class="opt-chevron">&#8964;</span></div>
<div class="opt-group-body">
<div class="opt-controls">
<div class="opt-field wide"><label>Style instruction</label><input class="opt-style-instruct" type="text" placeholder="cheerful, calm, excited"></div>
<div class="opt-field wide"><label>Style backend</label><select class="opt-style-backend">${styleBackendOptions('customvoice', true)}</select></div>
@ -4372,7 +4436,6 @@ function makeVoiceRow(v) {
</div>
<p class="opt-group-note">Preview first. Saving creates a new active WAV voice from the current reference text. Same-voice style only works when the selected backend knows this voice and honors <code>instruct</code>; Base/Streaming are fastest but often ignore style.</p>
<div class="backend-help opt-style-backend-help"></div>
</div>
<div class="opt-style-preview-box">
<div class="opt-controls">
<button class="btn-secondary opt-preview-style">Preview style</button>
@ -4381,8 +4444,10 @@ function makeVoiceRow(v) {
<audio class="opt-style-audio" controls></audio>
</div>
</div>
<div class="opt-group opt-maintenance">
<div class="opt-group-title">Loudness</div>
</div>
<div class="opt-group opt-maintenance open">
<div class="opt-group-title">Loudness <span class="opt-chevron">&#8964;</span></div>
<div class="opt-group-body">
<div class="opt-controls">
<div class="opt-field"><label>Target dBFS</label><input class="opt-target-db" type="number" step="0.5" value="-20"></div>
<button class="btn-secondary opt-db-minus">-</button>
@ -4394,6 +4459,7 @@ function makeVoiceRow(v) {
<span class="opt-restart-note" hidden>Restart TTS before benchmarking.</span>
</div>
</div>
</div>
<div class="opt-status optimizer-workflow">Click the pencil to load waveform and tools.</div>
</div>
</div>

View File

@ -25,13 +25,6 @@
<button class="btn-secondary vl-tb-btn" id="copy-active-voices-btn" title="Copy active voice names">Copy active</button>
</div>
<div class="vl-filters">
<input type="search" id="library-filter-text" placeholder="Search voices&#x2026;" autocomplete="off" class="vl-search">
<select id="library-filter-lang" title="Language"><option value="">All</option></select>
<select id="library-filter-type" title="Type"><option value="">All types</option></select>
<label class="vl-disabled-label"><input type="checkbox" id="show-disabled-cb"> Disabled</label>
</div>
<!-- TTS synth mode + preview sentence -->
<div class="vl-synth-panel">
<div class="vl-synth-mode-row">
@ -51,6 +44,13 @@
</div>
</div>
<div class="vl-filters">
<input type="search" id="library-filter-text" placeholder="Search voices&#x2026;" autocomplete="off" class="vl-search">
<select id="library-filter-lang" title="Language"><option value="">All</option></select>
<select id="library-filter-type" title="Type"><option value="">All types</option></select>
<label class="vl-disabled-label"><input type="checkbox" id="show-disabled-cb"> Disabled</label>
</div>
<div class="benchmark-confirm" id="benchmark-confirm" hidden role="group" aria-live="polite">
<strong id="benchmark-confirm-title">Benchmark active voices?</strong>
<span id="benchmark-confirm-text"></span>

View File

@ -46,7 +46,7 @@ body {
letter-spacing: -0.01em; line-height: 1.2;
}
.sidebar-brand p {
font-size: 11px; color: var(--subtext); margin-top: 3px;
font-size: 14px; color: var(--subtext); margin-top: 3px;
}
.sidebar-nav { flex: 1; padding: 8px 0; }
.nav-group-label {
@ -62,7 +62,7 @@ body {
background: var(--panel); border: 1px solid var(--border);
border-radius: 10px; padding: 1px 6px; min-width: 18px; text-align: center;
}
.nav-chevron { font-size: 11px; color: var(--subtext); transition: transform .2s; margin-left: 2px; }
.nav-chevron { font-size: 14px; color: var(--subtext); transition: transform .2s; margin-left: 2px; }
.nav-chevron.open { transform: rotate(180deg); }
.nav-tree { display: none; flex-direction: column; padding: 2px 0 6px; }
.nav-tree.open { display: flex; }
@ -73,7 +73,7 @@ body {
}
.nav-tree-item:hover { background: var(--panel); color: var(--text); }
.nav-tree-item.is-active { color: var(--accent); font-weight: 700; }
.ntc { font-size: 11px; color: var(--subtext); font-weight: 500; }
.ntc { font-size: 14px; color: var(--subtext); font-weight: 500; }
.nav-tree-item.is-active .ntc { color: var(--accent); }
.nav-item {
@ -113,8 +113,10 @@ body {
.page-section.is-active { display: block; }
.section-head {
display: flex; align-items: center; gap: 14px;
padding-bottom: 14px; border-bottom: 2px solid var(--border);
padding: 20px; border-bottom: 2px solid var(--border);
margin-bottom: 20px;
background: var(--surface);
border-radius: var(--radius);
}
.section-icon { font-size: 26px; line-height: 1; }
.section-title h2 {
@ -223,7 +225,7 @@ input.id-invalid { border-color: var(--red) !important; }
/* ── Helper details ─────────────────────────────────────────────────────── */
details.helper { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); }
details.helper summary { padding: 10px 14px; cursor: pointer; font-size: 13px; color: var(--subtext); user-select: none; list-style: none; }
details.helper summary::before { content: '\25B6 '; font-size: 11px; }
details.helper summary::before { content: '\25B6 '; font-size: 14px; }
details.helper[open] summary::before { content: '\25BC '; }
details.helper .helper-body { padding: 12px 14px 14px; display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
.field { display: flex; flex-direction: column; gap: 5px; }
@ -247,10 +249,10 @@ audio { width: 100%; }
/* ── Voice Design ───────────────────────────────────────────────────────── */
.design-hints { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12px; color: var(--subtext); }
.design-hints th, .design-hints td { text-align: left; vertical-align: top; padding: 7px 9px; border-bottom: 1px solid var(--border); }
.design-hints th { color: var(--text); font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }
.design-hints th { color: var(--text); font-size: 14px; text-transform: uppercase; letter-spacing: .06em; }
.design-samples-details summary { cursor: pointer; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; color: var(--subtext); user-select: none; list-style: none; }
.design-samples-details summary::-webkit-details-marker { display: none; }
.design-samples-details summary::before { content: '\25B6'; color: var(--accent); font-size: 11px; }
.design-samples-details summary::before { content: '\25B6'; color: var(--accent); font-size: 14px; }
.design-samples-details[open] summary::before { content: '\25BC'; }
.design-samples-details summary span:first-child { font-size: 12px; font-weight: 700; color: var(--subtext); text-transform: uppercase; letter-spacing: .09em; }
.design-samples-details .design-sample-header { margin-top: 12px; }
@ -258,7 +260,7 @@ audio { width: 100%; }
.preset-row input, .preset-row select { background: var(--panel); border: 1px solid var(--border); color: var(--text); border-radius: var(--radius); padding: 8px 12px; font-size: 14px; }
.design-sample-grid { display: grid; grid-template-columns: minmax(150px,1fr) 52px 86px minmax(220px,1.4fr) minmax(220px,1.4fr) 176px; gap: 8px; align-items: center; }
.design-sample-header { padding: 8px 10px 10px; border-bottom: 1px solid var(--border); }
.design-sample-header div { white-space: nowrap; overflow: hidden; font-size: 11px; font-weight: 700; color: var(--subtext); text-transform: uppercase; letter-spacing: .07em; }
.design-sample-header div { white-space: nowrap; overflow: hidden; font-size: 14px; font-weight: 700; color: var(--subtext); text-transform: uppercase; letter-spacing: .07em; }
.qwen-sample { border: 1px solid var(--border); border-radius: 6px; background: var(--panel); padding: 8px 10px; transition: border-color .15s; }
.qwen-sample:hover { border-color: var(--accent); }
.qwen-sample strong { color: var(--accent); font-size: 13px; }
@ -298,7 +300,7 @@ audio { width: 100%; }
.settings-actions { padding-top: 12px; margin-top: 2px; border-top: 1px solid var(--border); }
.setup-intro { background: rgba(37,99,235,.06); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; color: var(--subtext); font-size: 13px; line-height: 1.55; }
.setup-intro strong { color: var(--text); }
.s-group { font-size: 11px; font-weight: 700; color: var(--subtext); text-transform: uppercase; letter-spacing: .09em; padding-top: 6px; border-top: 1px solid var(--border); }
.s-group { font-size: 14px; font-weight: 700; color: var(--subtext); text-transform: uppercase; letter-spacing: .09em; padding-top: 6px; border-top: 1px solid var(--border); }
.s-field { display: flex; flex-direction: column; gap: 5px; }
.s-field label { font-size: 13px; color: var(--subtext); font-weight: 500; }
.s-field input, .s-field select, .s-field textarea { background: var(--panel); border: 1px solid var(--border); color: var(--text); border-radius: var(--radius); padding: 9px 14px; font-size: 14px; }
@ -376,7 +378,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
#benchmark-sample-text { min-height: 62px; resize: vertical; background: var(--panel); border: 1px solid var(--border); color: var(--text); border-radius: var(--radius); padding: 10px 14px; font-size: 14px; font-family: inherit; width: 100%; }
.benchmark-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.volume-actions { align-items: flex-end; }
.target-db-field { display: flex; flex-direction: column; gap: 4px; flex: 0 0 112px; color: var(--subtext); font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }
.target-db-field { display: flex; flex-direction: column; gap: 4px; flex: 0 0 112px; color: var(--subtext); font-size: 14px; text-transform: uppercase; letter-spacing: .06em; }
.target-db-field input { width: 112px; background: var(--panel); border: 1px solid var(--border); color: var(--text); border-radius: var(--radius); padding: 8px 10px; font-size: 14px; }
.play-mode-field select { width: 100%; }
.benchmark-progress { margin-top: 10px; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--panel); display: flex; flex-direction: column; gap: 7px; }
@ -504,12 +506,12 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.vr-note input { width: 100%; background: transparent; border: none; border-bottom: 1px solid var(--border); color: var(--text); font-size: 13px; padding: 3px 5px; font-family: inherit; outline: none; }
.vr-note input:focus { border-bottom-color: var(--accent); }
.vr-note input::placeholder { color: var(--border); }
.vr-row-subtle { color: var(--subtext); font-size: 11px; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vr-row-subtle { color: var(--subtext); font-size: 14px; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vr-optimizer { grid-column: auto; display: none; margin: 6px 0 0; width: 100%; box-sizing: border-box; }
.vl-row.edit-open .vr-optimizer { display: block; }
.optimizer-grid { display: grid; grid-template-columns: minmax(280px,1.2fr) minmax(280px,1fr); gap: 12px; align-items: start; }
.opt-group { margin-top: 20px; margin-right: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); padding: 10px; min-width: 0; }
.opt-group-title { font-size: 11px; font-weight: 800; color: var(--accent); text-transform: uppercase; letter-spacing: .08em; margin-bottom: 8px; }
.opt-group { box-shadow: var(--shadow); margin-top: 20px; margin-right: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); padding: 10px; min-width: 0; }
.opt-group-title { font-size: 14px; font-weight: 800; color: var(--accent); text-transform: uppercase; letter-spacing: .08em; margin-bottom: 8px; }
.opt-group-note { font-size: 12px; color: var(--subtext); line-height: 1.4; margin-top: 7px; }
.opt-compare-panel { grid-column: 1 / -1; }
.opt-compare-grid { display: grid; grid-template-columns: repeat(2, minmax(240px,1fr)); gap: 10px; }
@ -583,13 +585,13 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.sample-read-box, .mic-monitor-box { border: 1px solid rgba(37,99,235,.18); border-radius: 6px; background: var(--surface); padding: 12px; }
.mic-monitor-box { padding: 10px; display: flex; flex-direction: column; gap: 8px; }
.sample-head, .mic-monitor-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 6px; }
.sample-head label, .mic-monitor-head span:first-child { font-size: 11px; font-weight: 700; color: var(--accent); text-transform: uppercase; letter-spacing: .06em; }
.sample-head label, .mic-monitor-head span:first-child { font-size: 14px; font-weight: 700; color: var(--accent); text-transform: uppercase; letter-spacing: .06em; }
.sample-head select { background: var(--bg); border: 1px solid var(--border); color: var(--text); border-radius: 6px; padding: 6px 8px; font-size: 13px; min-width: 150px; }
.sample-sentence { width: 100%; min-height: 120px; font-size: 16px; line-height: 1.55; color: var(--text); background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--accent); border-radius: 6px; padding: 10px 12px; resize: vertical; font-family: inherit; }
.sample-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 8px; }
.sample-actions button { padding: 6px 10px; font-size: 12px; }
.recording-tips { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.recording-tip { font-size: 11px; color: var(--subtext); border: 1px solid rgba(37,99,235,.25); border-radius: 999px; padding: 3px 8px; background: rgba(37,99,235,.07); }
.recording-tip { font-size: 14px; color: var(--subtext); border: 1px solid rgba(37,99,235,.25); border-radius: 999px; padding: 3px 8px; background: rgba(37,99,235,.07); }
.meter-readout { font-family: monospace; color: var(--subtext); font-size: 12px; white-space: nowrap; }
.mic-meter { display: grid; grid-template-columns: repeat(18, 1fr); gap: 3px; height: 24px; align-items: end; margin-bottom: 8px; }
.mic-meter .bar { height: 7px; border-radius: 3px; background: var(--border); transition: height .08s linear, background .08s linear, opacity .08s linear; opacity: .55; }
@ -656,7 +658,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.routing-toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.routing-grid { display: grid; grid-template-columns: 42px minmax(104px,.7fr) minmax(92px,.55fr) 82px minmax(112px,.6fr) minmax(140px,1fr) minmax(120px,.65fr) minmax(120px,.65fr) 42px; gap: 8px; align-items: center; }
.routing-header { padding: 8px 10px 10px; border-bottom: 1px solid var(--border); }
.routing-header div { font-size: 11px; font-weight: 700; color: var(--subtext); text-transform: uppercase; letter-spacing: .07em; }
.routing-header div { font-size: 14px; font-weight: 700; color: var(--subtext); text-transform: uppercase; letter-spacing: .07em; }
.routing-row { border: 1px solid var(--border); border-radius: 6px; background: var(--surface); padding: 8px 10px; }
.routing-row input, .routing-row select { width: 100%; min-width: 0; background: var(--bg); border: 1px solid var(--border); color: var(--text); border-radius: 6px; padding: 7px 8px; font-size: 13px; }
.route-sound-cell { display: grid; grid-template-columns: minmax(0,1fr) auto auto; gap: 5px; align-items: center; }
@ -748,7 +750,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.voices-empty-divider {
display: flex; align-items: center; justify-content: center;
width: 40px; flex-shrink: 0;
font-size: 11px; font-weight: 700; color: var(--subtext);
font-size: 14px; font-weight: 700; color: var(--subtext);
text-transform: uppercase; letter-spacing: .08em;
background: var(--panel); border-left: 1px solid var(--border); border-right: 1px solid var(--border);
}
@ -759,7 +761,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
color: var(--text); font-family: monospace;
}
.voices-folder-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(37,99,235,.12); }
.voices-empty-hint { font-size: 11.5px; color: var(--subtext); opacity: .75; line-height: 1.7; margin-top: 2px; }
.voices-empty-hint { font-size: 14.5px; color: var(--subtext); opacity: .75; line-height: 1.7; margin-top: 2px; }
.voices-empty-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.voices-empty-action-btn { width: 100%; }
@ -781,7 +783,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
display: flex; flex-wrap: wrap; gap: 4px;
padding: 8px 10px 7px; border-bottom: 1px solid var(--border);
}
.vl-tb-btn { padding: 3px 8px !important; font-size: 11px !important; flex: 0 0 auto; }
.vl-tb-btn { padding: 3px 8px !important; font-size: 14px !important; flex: 0 0 auto; }
.vl-filters {
display: flex; align-items: center; gap: 5px; flex-wrap: wrap;
padding: 7px 10px; border-bottom: 1px solid var(--border);
@ -793,14 +795,14 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
}
.vl-search:focus { outline: none; border-color: var(--accent); }
.vl-filters select {
padding: 4px 5px; font-size: 11.5px; border: 1px solid var(--border);
padding: 4px 5px; font-size: 14.5px; border: 1px solid var(--border);
border-radius: var(--radius); background: var(--bg); color: var(--text); max-width: 80px;
}
.vl-disabled-label { font-size: 11.5px; color: var(--subtext); white-space: nowrap; cursor: pointer; gap: 3px; display: flex; align-items: center; }
.vl-disabled-label { font-size: 14.5px; color: var(--subtext); white-space: nowrap; cursor: pointer; gap: 3px; display: flex; align-items: center; }
#voice-list { flex: 1; overflow-y: auto; min-height: 0; }
.vl-footer {
padding: 7px 10px; border-top: 1px solid var(--border);
font-size: 11.5px; color: var(--subtext);
font-size: 14.5px; color: var(--subtext);
display: flex; align-items: center; justify-content: space-between; gap: 6px;
}
.vl-add-btn { padding: 4px 9px !important; font-size: 11.5px !important; }
@ -863,7 +865,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.vl-gender-label.g-f { color: #e91e8c; }
.vl-gender-label.g-m { color: #0ea5e9; }
.vl-gender-label.g-n { color: #8b5cf6; }
.vl-flag { font-size: 11px; color: var(--subtext); }
.vl-flag { font-size: 14px; color: var(--subtext); }
.vl-dbfs { display: none; }
/* Synth mode panel (segmented control + preview input) */
@ -882,7 +884,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.vl-synth-seg-btn.active { background: var(--teal); color: #fff; }
.vl-synth-seg-btn:not(.active):hover { background: var(--border); color: var(--text); }
.vl-preview-text-wrap { padding: 0 8px 6px; }
.vl-preview-input { width: 100%; font-size: 11px; background: var(--bg); border: 1px solid var(--border); border-radius: 5px; color: var(--text); padding: 3px 7px; font-family: var(--font); }
.vl-preview-input { width: 100%; font-size: 14px; background: var(--bg); border: 1px solid var(--border); border-radius: 5px; color: var(--text); padding: 3px 7px; font-family: var(--font); }
.vl-preview-input:focus { outline: none; border-color: var(--accent); }
.vl-preview-text-wrap.hidden { display: none; }
@ -891,8 +893,6 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
flex: 1; min-width: 0;
background: var(--bg);
display: flex; flex-direction: column; overflow: hidden;
border: 1px solid var(--border); border-radius: var(--radius);
box-shadow: var(--shadow);
}
.inspector-placeholder {
height: 100%; display: flex; flex-direction: column;
@ -939,7 +939,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.insp-hd-row1 { display: flex; align-items: center; gap: 8px; }
.insp-disp-name {
flex: 1; min-width: 0;
font-size: 16px; font-weight: 600; color: var(--text); margin: 0; line-height: 1.3;
font-size: 18px; font-weight: 600; color: var(--text); margin: 0; line-height: 1.3;
cursor: default; user-select: text; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.insp-disp-name:hover { color: var(--accent); }
@ -955,7 +955,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.insp-hd-row2 { display: flex; align-items: center; gap: 5px; }
.insp-id-block { display: flex; align-items: center; gap: 4px; flex: 1; min-width: 0; }
.insp-full-id {
font-size: 10px; color: var(--subtext); font-family: monospace; font-weight: 400;
font-size: 14px; color: var(--subtext); font-family: monospace; font-weight: 400;
cursor: default; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
.insp-full-id:hover { color: var(--accent); }
@ -966,8 +966,8 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
}
.insp-copy-id-btn:hover { color: var(--accent); border-color: var(--accent); }
.insp-actions-active { display: flex; align-items: center; flex-shrink: 0; }
.insp-actions-active .vr-detail-active { display: flex; align-items: center; gap: 5px; }
.insp-actions-active .vr-detail-active > label:first-child { font-size: 10px; font-weight: 500; color: var(--subtext); }
.insp-actions-active .vr-detail-active { display: contents; align-items: center; gap: 5px; }
.insp-actions-active .vr-detail-active > label:first-child { padding-right: 10px; font-weight: 500; color: var(--subtext); }
.insp-actions-active .toggle { width: 32px; height: 18px; }
.insp-actions-active .t-slider::before { height: 12px; width: 12px; left: 3px; bottom: 3px; }
.insp-actions-active .toggle input:checked + .t-slider::before { transform: translateX(14px); }
@ -982,13 +982,13 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.insp-note-slot .vr-note label { display: none; }
.insp-note-slot .vr-note .vr-inline { display: flex; flex: 1; }
.insp-note-slot .vr-note .vr-inline input {
width: 100%; padding: 2px 0; font-size: 11px; font-style: italic; font-family: inherit;
width: 100%; padding: 2px 0; font-size: 14px; font-style: italic; font-family: inherit;
border: none; background: transparent; color: var(--text); outline: none; transition: color .15s;
}
.insp-note-slot .vr-note .vr-inline input::placeholder { color: var(--subtext); opacity: .6; }
.insp-note-slot .vr-note .vr-inline input:focus { font-style: normal; }
.insp-note-slot .ref-transcribe-btn { display: none !important; }
.insp-actions-delete { flex-shrink: 0; display: flex; align-items: center; padding-left: 8px; }
.insp-actions-delete { flex-shrink: 0; display: flex; align-items: center; padding-left: 20px; margin-right: 20px }
.insp-actions-delete .vr-detail-delete { display: flex; align-items: center; }
.insp-actions-delete .delete-btn {
background: none; border: none; color: var(--subtext); font-size: 10.5px;
@ -1020,20 +1020,20 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.insp-flag-icon { display: flex; align-items: center; line-height: 1; }
.insp-flag-icon .fi { width: 22px; height: 16px; background-size: cover; background-position: center; border-radius: 2px; display: inline-block; flex-shrink: 0; }
.insp-flag-sel {
font-size: 11px; padding: 1px 4px; border: 1px solid var(--accent);
font-size: 14px; padding: 1px 4px; border: 1px solid var(--accent);
border-radius: 4px; background: var(--surface); color: var(--text); outline: none;
}
/* Lang wrap — dblclick to edit language */
.insp-lang-wrap { display: flex; align-items: center; cursor: default; }
.insp-lang-code { font-size: 11.5px; font-weight: 600; color: var(--subtext); }
.insp-lang-code { font-size: 14.5px; font-weight: 600; color: var(--subtext); }
.insp-lang-sel {
font-size: 11px; padding: 1px 4px; border: 1px solid var(--accent);
font-size: 14px; padding: 1px 4px; border: 1px solid var(--accent);
border-radius: 4px; background: var(--surface); color: var(--text); outline: none;
}
/* Gender label — click to cycle */
.insp-gender-label {
font-size: 11.5px; color: var(--subtext); cursor: pointer; user-select: none;
font-size: 14.5px; color: var(--subtext); cursor: pointer; user-select: none;
}
.insp-gender-label:hover { color: var(--text); }
@ -1047,7 +1047,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
/* Tag input — inline last item in subtitle */
.insp-tag-input {
border: none; background: transparent; color: var(--subtext);
font-size: 11.5px; font-family: inherit; outline: none;
font-size: 14.5px; font-family: inherit; outline: none;
padding: 0 6px; min-width: 50px;
transition: background .15s, color .15s;
}
@ -1055,6 +1055,22 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.insp-tag-input:hover { color: var(--text); }
.insp-tag-input:focus { background: var(--panel); color: var(--text); }
/* Tag autocomplete dropdown */
.tag-suggest {
position: fixed; z-index: 9998;
background: var(--surface); border: 1px solid var(--border);
border-radius: 6px; box-shadow: 0 4px 16px rgba(0,0,0,0.12);
max-height: 220px; overflow-y: auto; padding: 3px 0;
}
.tag-suggest-item {
display: block; width: 100%; text-align: left;
padding: 5px 12px; font-size: 12px; font-family: inherit;
background: none; border: none; color: var(--text);
cursor: pointer; white-space: nowrap; border-radius: 0;
transition: background .1s;
}
.tag-suggest-item:hover { background: var(--panel); filter: none; }
.insp-save-btn { white-space: nowrap; font-size: 12px; padding: 4px 10px; }
@ -1082,7 +1098,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
}
.insp-picker-item:hover { background: var(--panel); filter: none; }
.insp-picker-item .ipi-code {
font-family: monospace; font-weight: 700; font-size: 11px;
font-family: monospace; font-weight: 700; font-size: 14px;
color: var(--accent); min-width: 28px; flex-shrink: 0;
}
.insp-picker-empty { padding: 10px; font-size: 12px; color: var(--subtext); text-align: center; }
@ -1095,19 +1111,33 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.inspector-body .optimizer-grid { display: contents; }
.inspector-body .opt-status { display: none !important; }
/* Each opt-group as a clean card */
/* Each opt-group as a clean collapsible card */
.inspector-body .opt-group {
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden;
}
.inspector-body .opt-group-title {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
display: flex; align-items: center; gap: 8px;
padding: 9px 14px; font-size: 10.5px; font-weight: 700; letter-spacing: .05em;
text-transform: uppercase; color: var(--subtext);
border-bottom: 1px solid var(--border); background: var(--panel);
background: var(--panel); cursor: pointer; user-select: none;
transition: background .15s;
}
.inspector-body .opt-group-title:hover { background: var(--border); }
.opt-chevron {
margin-left: auto; font-size: 14px; line-height: 1; color: var(--subtext);
transition: transform .2s; display: inline-block; flex-shrink: 0;
}
.inspector-body .opt-group.open > .opt-group-title { border-bottom: 1px solid var(--border); }
.inspector-body .opt-group.open .opt-chevron { transform: none; }
.inspector-body .opt-group:not(.open) .opt-chevron { transform: rotate(-90deg); }
/* Body: smooth height transition */
.opt-group-body { overflow: hidden; }
.inspector-body .opt-group:not(.open) > .opt-group-body { display: none; }
.inspector-body .opt-group canvas.opt-wave { display: block; width: 100%; height: 72px; background: var(--bg); }
.inspector-body .opt-group > .opt-controls { padding: 10px 14px; display: flex; flex-wrap: wrap; gap: 7px; align-items: center; }
.inspector-body .opt-group > textarea {
.inspector-body .opt-group .opt-group-body > .opt-controls { padding: 10px 14px; display: flex; flex-wrap: wrap; gap: 7px; align-items: center; }
.inspector-body .opt-group .opt-group-body > textarea {
display: block; width: 100%; box-sizing: border-box; padding: 10px 14px; margin: 0;
background: transparent; border: none; outline: none; resize: vertical;
font-size: 13px; color: var(--text); font-family: inherit; min-height: 80px;
@ -1158,7 +1188,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.inspector-body .vr-inline { display: flex; align-items: center; gap: 8px; }
.opt-group-meta {
font-size: 11px; font-weight: 600; color: var(--accent);
font-size: 14px; font-weight: 600; color: var(--accent);
text-transform: none; letter-spacing: 0; flex-shrink: 0;
}
@ -1195,7 +1225,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.llm-sec-title { font-size: 15px; font-weight: 700; color: var(--text); margin: 0 0 4px; }
.llm-sec-note { font-size: 13px; color: var(--subtext); margin: 0; line-height: 1.5; max-width: 640px; }
.llm-free-badge {
font-size: 11px; font-weight: 700; letter-spacing: .02em;
font-size: 14px; font-weight: 700; letter-spacing: .02em;
color: var(--green); background: rgba(22,163,74,.10); border: 1px solid rgba(22,163,74,.22);
border-radius: 12px; padding: 3px 12px; white-space: nowrap; flex-shrink: 0; align-self: flex-start;
}
@ -1227,7 +1257,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
background: var(--panel); border: 1px solid var(--border);
}
.llm-card-name { font-size: 14px; font-weight: 700; color: var(--text); line-height: 1.2; }
.llm-card-sub { font-size: 11px; color: var(--subtext); margin-top: 2px; line-height: 1.35; }
.llm-card-sub { font-size: 14px; color: var(--subtext); margin-top: 2px; line-height: 1.35; }
.llm-tier-badge {
margin-left: auto; flex-shrink: 0;
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
@ -1240,14 +1270,14 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
/* Quick-stat chips */
.llm-card-stats { display: flex; gap: 5px; flex-wrap: wrap; }
.llm-card-stats span {
font-size: 11px; color: var(--subtext);
font-size: 14px; color: var(--subtext);
background: var(--panel); border: 1px solid var(--border);
padding: 2px 9px; border-radius: 10px; white-space: nowrap;
}
/* API key row */
.llm-field-row { display: flex; align-items: center; gap: 7px; }
.llm-label { font-size: 11px; font-weight: 600; color: var(--subtext); white-space: nowrap; flex-shrink: 0; }
.llm-label { font-size: 14px; font-weight: 600; color: var(--subtext); white-space: nowrap; flex-shrink: 0; }
.llm-input {
flex: 1; min-width: 0; font-size: 12px; font-family: monospace;
padding: 5px 10px; border: 1px solid var(--border); border-radius: 6px;
@ -1255,7 +1285,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
}
.llm-input:focus { border-color: var(--accent); background: var(--surface); box-shadow: 0 0 0 3px rgba(37,99,235,.10); }
.llm-link {
font-size: 11px; color: var(--accent); text-decoration: none; white-space: nowrap;
font-size: 14px; color: var(--accent); text-decoration: none; white-space: nowrap;
flex-shrink: 0; font-weight: 600; transition: opacity .15s;
}
.llm-link:hover { opacity: .75; }
@ -1264,11 +1294,11 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.llm-endpoint {
display: flex; align-items: center; gap: 8px;
padding: 5px 10px; background: var(--panel); border-radius: 6px;
border: 1px solid var(--border); font-size: 11px; overflow: hidden;
border: 1px solid var(--border); font-size: 14px; overflow: hidden;
}
.llm-endpoint-label { color: var(--subtext); font-weight: 600; white-space: nowrap; flex-shrink: 0; }
.llm-endpoint code {
font-family: monospace; font-size: 11px; color: var(--teal);
font-family: monospace; font-size: 14px; color: var(--teal);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
@ -1288,12 +1318,12 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
}
.llm-link-inline { color: var(--accent); text-decoration: none; font-weight: 600; }
.llm-link-inline:hover { text-decoration: underline; }
.llm-info-note { font-size: 11px; color: var(--subtext); font-style: italic; }
.llm-info-note { font-size: 14px; color: var(--subtext); font-style: italic; }
.llm-info-note code { font-family: monospace; color: var(--teal); font-style: normal; }
/* ── Local services ────────────────────────────────────── */
.llm-local-cat {
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
color: var(--subtext); margin: 0 0 10px;
padding-bottom: 7px; border-bottom: 1px solid var(--border);
}
@ -1323,10 +1353,10 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.llm-local-url {
display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
font-size: 11px; font-weight: 600; color: var(--subtext);
font-size: 14px; font-weight: 600; color: var(--subtext);
background: var(--panel); padding: 4px 9px; border-radius: 5px; border: 1px solid var(--border);
}
.llm-local-url code { font-family: monospace; font-size: 11px; color: var(--teal); }
.llm-local-url code { font-family: monospace; font-size: 14px; color: var(--teal); }
/* Code snippet block with dark theme */
.llm-local-snippet { background: #1b1f2e; border-radius: 8px; overflow: hidden; }
@ -1343,10 +1373,10 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.llm-copy-btn:hover { background: rgba(255,255,255,.2); filter: none; }
.llm-local-snippet pre {
margin: 0; padding: 10px 12px; color: #9ecfb0; font-family: monospace;
font-size: 11px; line-height: 1.65; overflow-x: auto; white-space: pre;
font-size: 14px; line-height: 1.65; overflow-x: auto; white-space: pre;
}
.llm-local-link {
font-size: 11.5px; color: var(--accent); text-decoration: none; font-weight: 600;
font-size: 14.5px; color: var(--accent); text-decoration: none; font-weight: 600;
align-self: flex-start;
}
.llm-local-link:hover { text-decoration: underline; }