Characters/Cast gains a "Casting" button back to the active casting session, the pipeline stepper renders on the Library section, and the stepper's Cast Characters stop navigates instead of side-effect-running sheet generation. Prompt generation: 4096-token budget (1600 truncated the four-prompt JSON so two fields silently arrived empty), truncated answers salvage completed fields, all-empty responses fail loudly, and partial results name the missing prompts. The PDF-extraction progress pill is enlarged and vertically centered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
510 lines
23 KiB
JavaScript
510 lines
23 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
const TAB_SECTION_MAP = {
|
|
library: 's-voices',
|
|
source: 's-clone',
|
|
save: 's-clone',
|
|
design: 's-design',
|
|
custom: 's-design',
|
|
getvoices: 's-studio',
|
|
generation: 's-tryout',
|
|
'stt-tts': 's-tryout',
|
|
performance: 's-performance',
|
|
routing: 's-routing',
|
|
integrations: 's-connect',
|
|
howto: 's-connect',
|
|
settings: 's-settings',
|
|
llms: 's-llms'
|
|
};
|
|
|
|
const SECTIONS = ['s-voices', 's-clone', 's-design', 's-studio', 's-tryout', 's-rehearser', 's-reader', 's-performance', 's-routing', 's-connect', 's-settings', 's-llms', 's-conversation', 's-library'];
|
|
|
|
function storedSection() {
|
|
try { return localStorage.getItem('ttsvc_section') || ''; } catch (_) { return ''; }
|
|
}
|
|
|
|
function setStoredSection(sectionId) {
|
|
try { localStorage.setItem('ttsvc_section', sectionId); } catch (_) {}
|
|
}
|
|
|
|
function hashSection() {
|
|
var raw = String(location.hash || '').replace(/^#/, '').trim();
|
|
if (!raw) return '';
|
|
if (raw.indexOf('section=') === 0) raw = raw.slice(8);
|
|
raw = decodeURIComponent(raw).replace(/^\/?/, '');
|
|
return SECTIONS.includes(raw) ? raw : '';
|
|
}
|
|
|
|
function setSectionHash(sectionId) {
|
|
if (!SECTIONS.includes(sectionId)) return;
|
|
var next = '#' + encodeURIComponent(sectionId);
|
|
if (location.hash === next) return;
|
|
try { history.replaceState(null, '', location.pathname + location.search + next); }
|
|
catch (_) { location.hash = next; }
|
|
}
|
|
|
|
function runSideEffects(name) {
|
|
if ((name === 'source' || name === 'save') && typeof initCloneSampleText === 'function') initCloneSampleText();
|
|
if (name === 'library' && typeof scheduleVoiceLibraryLoad === 'function') scheduleVoiceLibraryLoad();
|
|
if ((name === 'integrations' || name === 'howto') && typeof renderIntegrationSnippets === 'function') {
|
|
if (typeof scheduleVoiceLibraryLoad === 'function' && !(window._voices && window._voices.length)) scheduleVoiceLibraryLoad(renderIntegrationSnippets);
|
|
renderIntegrationSnippets();
|
|
}
|
|
if (name === 'routing' && typeof loadRoutingTab === 'function') loadRoutingTab();
|
|
if (name === 'getvoices') {
|
|
if (typeof window.initElevenLabsBrowser === 'function') window.initElevenLabsBrowser();
|
|
if (typeof loadGetVoices === 'function') loadGetVoices();
|
|
}
|
|
if (name === 'performance' && typeof window.populateBatchBenchmarkFromLibrary === 'function') window.populateBatchBenchmarkFromLibrary();
|
|
if (name === 'performance' && typeof window.loadBenchmarkSectionData === 'function') window.loadBenchmarkSectionData();
|
|
}
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
// ── Tree open/close state — each tree is independent ─────────────────────
|
|
// Load persisted open state (default: open voices + the active section's tree)
|
|
var _treeOpen = {};
|
|
try { _treeOpen = JSON.parse(localStorage.getItem('ttsvc_trees') || '{}'); } catch(_) {}
|
|
|
|
// Every collapsible tree must be listed so _applyTreeStates toggles its
|
|
// open class + chevron. `section` (optional) auto-opens the tree (and its
|
|
// ancestor trees, handled in _openTreesForSection) when that section opens.
|
|
var TREES = [
|
|
{ tree: 'nav-voices-tree', chevron: 'nav-voices-chevron', section: 's-voices' },
|
|
{ tree: 'nav-tags-tree', chevron: 'nav-tags-chevron' },
|
|
{ tree: 'nav-actions-tree', chevron: 'nav-actions-chevron' },
|
|
{ tree: 'nav-speak-tree', chevron: 'nav-speak-chevron' },
|
|
{ tree: 'nav-reader-tree', chevron: 'nav-reader-chevron', section: 's-reader' },
|
|
{ tree: 'nav-rehearser-tree', chevron: 'nav-rehearser-chevron', section: 's-rehearser' },
|
|
{ tree: 'nav-library-tree', chevron: 'nav-library-chevron', section: 's-library' },
|
|
{ tree: 'nav-setup-tree', chevron: 'nav-setup-chevron' },
|
|
{ tree: 'nav-engines-tree', chevron: 'nav-engines-chevron', section: 's-llms' },
|
|
{ tree: 'nav-integrations-tree', chevron: 'nav-integrations-chevron' },
|
|
{ tree: 'nav-settings-tree', chevron: 'nav-settings-chevron', section: 's-settings' },
|
|
];
|
|
|
|
// Open the tree(s) for a section: the head's own tree (via TREES.section)
|
|
// plus every ancestor .nav-tree of the active nav element, so nested items
|
|
// become visible. Persists once if anything changed.
|
|
function _openTreesForSection(sectionId) {
|
|
var changed = false;
|
|
TREES.forEach(function (t) {
|
|
if (t.section === sectionId && !_treeOpen[t.tree]) { _treeOpen[t.tree] = true; changed = true; }
|
|
});
|
|
var el = document.querySelector('[data-nav-section="' + sectionId + '"]');
|
|
while (el) {
|
|
if (el.classList && el.classList.contains('nav-tree') && el.id && !_treeOpen[el.id]) {
|
|
_treeOpen[el.id] = true; changed = true;
|
|
}
|
|
el = el.parentElement;
|
|
}
|
|
if (changed) localStorage.setItem('ttsvc_trees', JSON.stringify(_treeOpen));
|
|
}
|
|
|
|
function _applyTreeStates() {
|
|
TREES.forEach(function (t) {
|
|
var open = !!_treeOpen[t.tree];
|
|
var treeEl = document.getElementById(t.tree);
|
|
var chevEl = document.getElementById(t.chevron);
|
|
if (treeEl) treeEl.classList.toggle('open', open);
|
|
if (chevEl) chevEl.classList.toggle('open', open);
|
|
});
|
|
}
|
|
|
|
// Show/hide an animated dot on a nav section head to signal a background process.
|
|
// Call setNavBusy('s-reader', true) when synthesis/playback starts, false when done.
|
|
window.setNavBusy = function (sectionId, busy) {
|
|
var el = document.querySelector('[data-nav-section="' + sectionId + '"]');
|
|
if (!el) return;
|
|
var dot = el.querySelector('.nav-busy-dot');
|
|
if (busy && !dot) {
|
|
dot = document.createElement('span');
|
|
dot.className = 'nav-busy-dot';
|
|
dot.title = 'Running…';
|
|
el.appendChild(dot);
|
|
} else if (!busy && dot) {
|
|
dot.remove();
|
|
}
|
|
};
|
|
|
|
window.toggleNavTree = function (treeId, chevronId) {
|
|
_treeOpen[treeId] = !_treeOpen[treeId];
|
|
localStorage.setItem('ttsvc_trees', JSON.stringify(_treeOpen));
|
|
_applyTreeStates();
|
|
};
|
|
|
|
function showSection(sectionId) {
|
|
if (!SECTIONS.includes(sectionId)) sectionId = 's-voices';
|
|
// Leaving the Read Aloud reader: stop playback so audio doesn't keep running
|
|
if (sectionId !== 's-reader' && typeof window.readerStop === 'function') window.readerStop();
|
|
// Capture before readerOnShow (async) eventually consumes it: a caller that
|
|
// requested a specific reader view via _readerStartView must not have that
|
|
// choice overridden by the cast-session restore below.
|
|
var _explicitReaderView = window._readerStartView;
|
|
if (sectionId === 's-reader' && typeof window.readerOnShow === 'function') window.readerOnShow();
|
|
// If a cast session panel is active, restore it instead of showing the main
|
|
// PDF view. Synchronous on purpose: callers that want a different view just
|
|
// call showReaderView(...) after navTo() and their choice wins — no deferred
|
|
// override racing whatever the caller set (the old setTimeout version hijacked
|
|
// the view a tick later and needed a global suppress flag to defeat).
|
|
if (sectionId === 's-reader' && !_explicitReaderView) {
|
|
var _castPnl = document.getElementById('reader-audiobook-panel');
|
|
if (_castPnl && _castPnl.querySelector('#ab-cv-feed') && typeof window.showReaderView === 'function') {
|
|
window.showReaderView('cast');
|
|
}
|
|
}
|
|
if (sectionId === 's-library' && typeof window.libraryRender === 'function') window.libraryRender(window._libraryView || 'books');
|
|
if (typeof window.initCollapsibleCards === 'function') window.initCollapsibleCards();
|
|
setStoredSection(sectionId);
|
|
setSectionHash(sectionId);
|
|
SECTIONS.forEach(function (id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.classList.toggle('is-active', id === sectionId);
|
|
});
|
|
document.querySelectorAll('[data-nav-section]').forEach(function (item) {
|
|
item.classList.toggle('active', item.dataset.navSection === sectionId);
|
|
});
|
|
|
|
// Navigating to a section auto-opens its tree AND every ancestor tree
|
|
// (so a nested item is actually visible), but never closes others.
|
|
_openTreesForSection(sectionId);
|
|
_applyTreeStates();
|
|
|
|
// When entering settings, show the active sub-page (default: connections)
|
|
if (sectionId === 's-settings') {
|
|
applySettingsPage(window._settingsSidebarCat || 'connections');
|
|
}
|
|
|
|
// When entering engines, show the active sub-page (default: llm) and load containers
|
|
if (sectionId === 's-llms') {
|
|
applyEnginesPage(window._enginesSidebarCat || 'llm');
|
|
if (typeof loadLocalContainers === 'function') loadLocalContainers();
|
|
}
|
|
|
|
var main = document.getElementById('main-content');
|
|
if (main) main.scrollTop = 0;
|
|
}
|
|
|
|
window.navTo = function (sectionId) {
|
|
showSection(sectionId);
|
|
var tabName = Object.keys(TAB_SECTION_MAP).find(function (k) {
|
|
return TAB_SECTION_MAP[k] === sectionId;
|
|
});
|
|
if (tabName) runSideEffects(tabName);
|
|
// Keep the Reader/Casting/Characters/Rehearser breadcrumb in sync with
|
|
// plain sidebar navigation (not just the crumb's own buttons).
|
|
if (typeof window.refreshWorkflowCrumbs === 'function') {
|
|
if (sectionId === 's-rehearser') window.refreshWorkflowCrumbs('rehearser');
|
|
else if (sectionId === 's-reader') window.refreshWorkflowCrumbs('source');
|
|
else if (sectionId === 's-library' && (window._libraryView || 'books') === 'characters') window.refreshWorkflowCrumbs('chars');
|
|
}
|
|
};
|
|
|
|
// Pulsing dot on the Read Aloud nav item while casting is in progress
|
|
window.setNavCastingBadge = function (active) {
|
|
var el = document.querySelector('[data-nav-section="s-reader"]');
|
|
if (!el) return;
|
|
var existing = el.querySelector('.nav-casting-dot');
|
|
if (active && !existing) {
|
|
var dot = document.createElement('span');
|
|
dot.className = 'nav-casting-dot';
|
|
el.appendChild(dot);
|
|
} else if (!active && existing) {
|
|
existing.remove();
|
|
}
|
|
};
|
|
|
|
window.switchTab = function (name) {
|
|
var sectionId = TAB_SECTION_MAP[name];
|
|
if (sectionId) showSection(sectionId);
|
|
runSideEffects(name);
|
|
return true;
|
|
};
|
|
|
|
// ── Voice tree sub-navigation ─────────────────────────────────────────────
|
|
window._voiceSidebarCat = 'all';
|
|
|
|
window._voiceTagFilter = '';
|
|
|
|
window.navVoicesCat = function (cat) {
|
|
navTo('s-voices');
|
|
window._voiceSidebarCat = cat;
|
|
window._voiceTagFilter = ''; // a fixed category clears any tag subfolder filter
|
|
document.querySelectorAll('[data-voice-cat]').forEach(function (el) {
|
|
el.classList.toggle('is-active', el.dataset.voiceCat === cat);
|
|
});
|
|
document.querySelectorAll('[data-voice-tag]').forEach(function (el) { el.classList.remove('is-active'); });
|
|
if (typeof renderVoiceList === 'function') renderVoiceList();
|
|
};
|
|
|
|
// Click a tag "subfolder" in the My Voices tree → filter the list to that tag.
|
|
window.navVoicesTag = function (tag) {
|
|
navTo('s-voices');
|
|
window._voiceTagFilter = (window._voiceTagFilter === tag) ? '' : tag; // toggle off if re-clicked
|
|
window._voiceSidebarCat = 'all';
|
|
document.querySelectorAll('[data-voice-cat]').forEach(function (el) {
|
|
el.classList.toggle('is-active', el.dataset.voiceCat === 'all' && !window._voiceTagFilter);
|
|
});
|
|
document.querySelectorAll('[data-voice-tag]').forEach(function (el) {
|
|
el.classList.toggle('is-active', el.dataset.voiceTag === window._voiceTagFilter);
|
|
});
|
|
if (typeof renderVoiceList === 'function') renderVoiceList();
|
|
};
|
|
|
|
window.updateVoiceTree = function (voices) {
|
|
if (!voices) return;
|
|
function n(fn) { return voices.filter(fn).length; }
|
|
function set(id, v) { var el = document.getElementById(id); if (el) el.textContent = v || ''; }
|
|
set('ntc-all', n(function () { return true; }));
|
|
set('ntc-favorites',n(function (v) { return (v.rating || 0) >= 4; }));
|
|
set('ntc-hidden', n(function (v) { return v.enabled === false; }));
|
|
set('nav-voices-count', voices.length);
|
|
window.buildTagsTree(voices);
|
|
};
|
|
|
|
// ── Tags tree — distinct voice tags with counts, plus the computed
|
|
// Cloned / Designed / Favourites predicates that moved out of Library ──
|
|
window.buildTagsTree = function (voices) {
|
|
voices = voices || window._voices;
|
|
var tree = document.getElementById('nav-tags-tree');
|
|
if (!tree || !voices) return;
|
|
tree.innerHTML = '';
|
|
function n(fn) { return voices.filter(fn).length; }
|
|
|
|
// Computed predicate rows (not literal tag strings) → navVoicesCat
|
|
var preds = [
|
|
{ cat: 'cloned', icon: 'microphone-variant', label: 'Cloned', count: n(function (v) { return v.has_ref; }) },
|
|
{ cat: 'designed', icon: 'auto-fix', label: 'Designed', count: n(function (v) { return !v.has_ref; }) },
|
|
{ cat: 'favorites', icon: 'star-outline', label: 'Favourites', count: n(function (v) { return (v.rating || 0) >= 4; }) },
|
|
];
|
|
preds.forEach(function (p) {
|
|
var el = document.createElement('div');
|
|
el.className = 'nav-tree-item' + (window._voiceSidebarCat === p.cat && !window._voiceTagFilter ? ' is-active' : '');
|
|
el.dataset.voiceCat = p.cat;
|
|
el.innerHTML = '<span class="mdi mdi-' + p.icon + '"></span> ' + p.label + ' <span class="ntc">' + (p.count || '') + '</span>';
|
|
el.addEventListener('click', function () { window.navVoicesCat(p.cat); });
|
|
tree.appendChild(el);
|
|
});
|
|
|
|
// Literal tag strings → navVoicesTag (filters via _voiceTagFilter)
|
|
var counts = {};
|
|
voices.forEach(function (v) {
|
|
String(v.tag || '').split(',').map(function (t) { return t.trim(); }).filter(Boolean)
|
|
.forEach(function (t) { counts[t] = (counts[t] || 0) + 1; });
|
|
});
|
|
var names = Object.keys(counts).sort(function (a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); });
|
|
names.forEach(function (tag) {
|
|
var el = document.createElement('div');
|
|
el.className = 'nav-tree-item' + (window._voiceTagFilter === tag ? ' is-active' : '');
|
|
el.dataset.voiceTag = tag;
|
|
var icon = document.createElement('span'); icon.className = 'mdi mdi-tag-outline'; el.appendChild(icon);
|
|
el.appendChild(document.createTextNode(' ' + tag + ' '));
|
|
var c = document.createElement('span'); c.className = 'ntc'; c.textContent = counts[tag]; el.appendChild(c);
|
|
el.addEventListener('click', function () { window.navVoicesTag(tag); });
|
|
tree.appendChild(el);
|
|
});
|
|
};
|
|
|
|
// ── Settings tree sub-navigation ──────────────────────────────────────────
|
|
window._settingsSidebarCat = 'connections';
|
|
|
|
window.navSettingsCat = function (cat) {
|
|
window._settingsSidebarCat = cat;
|
|
localStorage.setItem('ttsvc_settings_cat', cat);
|
|
navTo('s-settings'); // showSection will call applySettingsPage(cat)
|
|
if (cat === 'logs' && typeof loadSettingsLogs === 'function') loadSettingsLogs();
|
|
if (cat === 'about' && typeof renderSettingsAbout === 'function') renderSettingsAbout();
|
|
};
|
|
|
|
// ── Engines tree sub-navigation ───────────────────────────────────────────
|
|
function applyEnginesPage(cat) {
|
|
document.querySelectorAll('.s-engines-page').forEach(function (p) {
|
|
p.classList.toggle('is-active', p.dataset.page === cat);
|
|
});
|
|
document.querySelectorAll('[data-engines-cat]').forEach(function (el) {
|
|
el.classList.toggle('is-active', el.dataset.enginesCat === cat);
|
|
});
|
|
}
|
|
|
|
window._enginesSidebarCat = 'llm';
|
|
|
|
window.navEnginesCat = function (cat) {
|
|
window._enginesSidebarCat = cat;
|
|
localStorage.setItem('ttsvc_engines_cat', cat);
|
|
navTo('s-llms'); // showSection will call applyEnginesPage(cat)
|
|
if (typeof loadLocalContainers === 'function') loadLocalContainers();
|
|
};
|
|
|
|
// ── Rehearser tree sub-navigation ────────────────────────────────────────
|
|
window.navRehearserImpEx = function () {
|
|
navTo('s-rehearser');
|
|
// Clear phase highlights — Import/Export is its own panel, not a numbered phase
|
|
document.querySelectorAll('[data-rehearser-phase]').forEach(function (el) {
|
|
el.classList.remove('is-active');
|
|
});
|
|
document.getElementById('nav-reh-impex')?.classList.add('is-active');
|
|
if (typeof showRehImpEx === 'function') showRehImpEx();
|
|
else window._rehearserStartImpEx = true;
|
|
};
|
|
|
|
window.navRehearserPhase = function (phase) {
|
|
navTo('s-rehearser');
|
|
// highlight the correct tree item immediately
|
|
document.querySelectorAll('[data-rehearser-phase]').forEach(function (el) {
|
|
el.classList.toggle('is-active', Number(el.dataset.rehearserPhase) === phase);
|
|
});
|
|
// delegate to rehearser's own showPhase() when it's ready
|
|
if (typeof showPhase === 'function') {
|
|
showPhase(phase);
|
|
} else {
|
|
// rehearser.js not yet loaded — store the intent and let init pick it up
|
|
window._rehearserStartPhase = phase;
|
|
}
|
|
};
|
|
|
|
// Keep sidebar tree in sync when rehearser changes phase internally
|
|
window.onRehearserPhaseChange = function (phase) {
|
|
document.querySelectorAll('[data-rehearser-phase]').forEach(function (el) {
|
|
el.classList.toggle('is-active', Number(el.dataset.rehearserPhase) === phase);
|
|
});
|
|
};
|
|
|
|
// ── Reader tree sub-navigation ───────────────────────────────────────────
|
|
window.navReaderView = function (view) {
|
|
navTo('s-reader');
|
|
document.querySelectorAll('[data-reader-view]').forEach(function (el) {
|
|
el.classList.toggle('is-active', el.dataset.readerView === view);
|
|
});
|
|
if (typeof showReaderView === 'function') {
|
|
showReaderView(view);
|
|
} else {
|
|
window._readerStartView = view;
|
|
}
|
|
};
|
|
|
|
// Restore last-used section from hash/localStorage (fallback: My Voices)
|
|
var _savedSection = storedSection();
|
|
// On very first visit, default voices tree open
|
|
if (!localStorage.getItem('ttsvc_trees')) {
|
|
_treeOpen['nav-voices-tree'] = true;
|
|
}
|
|
window._settingsSidebarCat = localStorage.getItem('ttsvc_settings_cat') || 'connections';
|
|
window._enginesSidebarCat = localStorage.getItem('ttsvc_engines_cat') || 'llm';
|
|
var _startSection = hashSection() || ((_savedSection && SECTIONS.includes(_savedSection)) ? _savedSection : 's-voices');
|
|
showSection(_startSection);
|
|
|
|
window.addEventListener('hashchange', function () {
|
|
var section = hashSection();
|
|
if (section && section !== storedSection()) showSection(section);
|
|
});
|
|
var _startTab = Object.keys(TAB_SECTION_MAP).find(function (k) { return TAB_SECTION_MAP[k] === _startSection; }) || 'library';
|
|
runSideEffects(_startTab);
|
|
|
|
// ── Mobile sidebar drawer ────────────────────────────────────────────────
|
|
var sidebar = document.getElementById('sidebar');
|
|
var backdrop = document.getElementById('sidebar-backdrop');
|
|
var toggleBtn = document.getElementById('sidebar-toggle');
|
|
|
|
function openSidebar() {
|
|
if (!sidebar) return;
|
|
sidebar.classList.add('open');
|
|
if (backdrop) backdrop.classList.add('open');
|
|
document.body.style.overflow = 'hidden'; // prevent scroll behind drawer
|
|
}
|
|
function closeSidebar() {
|
|
if (!sidebar) return;
|
|
sidebar.classList.remove('open');
|
|
if (backdrop) backdrop.classList.remove('open');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
if (toggleBtn) toggleBtn.addEventListener('click', openSidebar);
|
|
if (backdrop) backdrop.addEventListener('click', closeSidebar);
|
|
|
|
// ── Desktop sidebar collapse/expand (button where the flag used to be) ────
|
|
try { if (localStorage.getItem('ttsvc_sidebar_collapsed') === '1') document.body.classList.add('sidebar-collapsed'); } catch (_) {}
|
|
window.toggleSidebarCollapsed = function () {
|
|
var collapsed = document.body.classList.toggle('sidebar-collapsed');
|
|
try { localStorage.setItem('ttsvc_sidebar_collapsed', collapsed ? '1' : '0'); } catch (_) {}
|
|
var btn = document.getElementById('sidebar-collapse-btn');
|
|
if (btn) btn.title = collapsed ? 'Expand sidebar' : 'Collapse sidebar';
|
|
if (!collapsed) _railTip.style.display = 'none';
|
|
};
|
|
|
|
// ── Icon-rail per-item label tooltip ────────────────────────────────────
|
|
var _railTip = document.createElement('div');
|
|
_railTip.id = 'nav-rail-tooltip';
|
|
document.body.appendChild(_railTip);
|
|
|
|
var _railTipActive = null;
|
|
var _sidebarEl = document.getElementById('sidebar');
|
|
|
|
_sidebarEl.addEventListener('mouseover', function (e) {
|
|
if (!document.body.classList.contains('sidebar-collapsed')) return;
|
|
var item = e.target.closest('.nav-item, .nav-tree-item, .nav-tree-head');
|
|
if (!item || item === _railTipActive) return;
|
|
var label = item.querySelector('.nav-label');
|
|
var text = '';
|
|
if (label) {
|
|
text = label.textContent.trim();
|
|
} else {
|
|
var clone = item.cloneNode(true);
|
|
clone.querySelectorAll('.mdi, .ntc, .nav-badge, .nav-chevron').forEach(function (el) { el.remove(); });
|
|
text = clone.textContent.replace(/\s+/g, ' ').trim();
|
|
}
|
|
if (!text) return;
|
|
var r = item.getBoundingClientRect();
|
|
_railTip.textContent = text;
|
|
_railTip.style.left = (r.right + 10) + 'px';
|
|
_railTip.style.top = (r.top + r.height / 2) + 'px';
|
|
_railTip.style.display = 'block';
|
|
_railTipActive = item;
|
|
});
|
|
|
|
_sidebarEl.addEventListener('mouseout', function (e) {
|
|
if (!e.relatedTarget || !e.relatedTarget.closest('#sidebar')) {
|
|
_railTip.style.display = 'none';
|
|
_railTipActive = null;
|
|
}
|
|
});
|
|
|
|
// Close drawer when a nav item is tapped on mobile
|
|
document.querySelectorAll('#sidebar .nav-item, #sidebar .nav-tree-item').forEach(function (el) {
|
|
el.addEventListener('click', function () {
|
|
if (window.innerWidth <= 767) closeSidebar();
|
|
});
|
|
});
|
|
|
|
// ── Mobile voice inspector: back button ──────────────────────────────────
|
|
// When a voice row is selected on mobile, show inspector full-height.
|
|
// The inspector JS will call window.onMobileInspectorOpen/Close as hooks.
|
|
window.onMobileInspectorOpen = function () {
|
|
var wb = document.querySelector('.voices-workbench');
|
|
if (!wb || window.innerWidth > 767) return;
|
|
wb.classList.add('mobile-inspector-open');
|
|
// Inject back button if not already there
|
|
var insp = document.getElementById('voices-inspector');
|
|
if (insp && !insp.querySelector('.insp-mobile-back')) {
|
|
var back = document.createElement('div');
|
|
back.className = 'insp-mobile-back';
|
|
back.innerHTML = '<span class="mdi mdi-arrow-left"></span> All voices';
|
|
back.addEventListener('click', function () { window.onMobileInspectorClose && window.onMobileInspectorClose(); });
|
|
insp.insertBefore(back, insp.firstChild);
|
|
}
|
|
};
|
|
window.onMobileInspectorClose = function () {
|
|
var wb = document.querySelector('.voices-workbench');
|
|
if (wb) wb.classList.remove('mobile-inspector-open');
|
|
var back = document.querySelector('.insp-mobile-back');
|
|
if (back) back.remove();
|
|
};
|
|
})();
|