feat: collapsible nested sidebar groups

Group headers (Voice Actions, Speak, Setup) are now collapsible parents;
Tags nests under Library; Integrations + Settings nest under Setup.
Navigating a section auto-opens its whole ancestor tree chain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-06-27 14:30:14 +02:00
parent 52994df70b
commit 73c27944ac
3 changed files with 142 additions and 73 deletions

View File

@ -128,74 +128,92 @@
<div class="nav-tree-item" data-voice-cat="favorites" onclick="navVoicesCat('favorites')"><span class="mdi mdi-star-outline"></span> Favourites <span class="ntc" id="ntc-favorites"></span></div>
<div class="nav-tree-item" data-voice-cat="hidden" onclick="navVoicesCat('hidden')"><span class="mdi mdi-eye-off-outline"></span> Hidden <span class="ntc" id="ntc-hidden"></span></div>
<div class="nav-tree-item" data-voice-cat="tools" onclick="navVoicesCat('tools')"><span class="mdi mdi-wrench-outline"></span> Tools</div>
<div class="nav-tree-head nav-subhead" id="nav-tags-head" onclick="toggleNavTree('nav-tags-tree','nav-tags-chevron')">
<span class="nav-icon"><span class="mdi mdi-tag-multiple-outline"></span></span>
<span class="nav-label">Tags</span>
<span class="nav-chevron" id="nav-tags-chevron"><span class="mdi mdi-chevron-down"></span></span>
</div>
<div class="nav-tree" id="nav-tags-tree"></div>
</div>
<div class="nav-item nav-tree-head" data-nav-section="s-voices" id="nav-tags-head" onclick="navTo('s-voices')">
<span class="nav-icon"><span class="mdi mdi-tag-multiple-outline"></span></span>
<span class="nav-label">Tags</span>
<span class="nav-chevron" id="nav-tags-chevron" onclick="event.stopPropagation();toggleNavTree('nav-tags-tree','nav-tags-chevron')"><span class="mdi mdi-chevron-down"></span></span>
<div class="nav-group-head" id="nav-actions-head" onclick="toggleNavTree('nav-actions-tree','nav-actions-chevron')">
<span class="nav-label">Voice Actions</span>
<span class="nav-chevron" id="nav-actions-chevron"><span class="mdi mdi-chevron-down"></span></span>
</div>
<div class="nav-tree" id="nav-tags-tree"></div>
<div class="nav-group-label">Voice Actions</div>
<div class="nav-item" data-nav-section="s-clone" onclick="navTo('s-clone')"> <span class="nav-icon"><span class="mdi mdi-microphone-variant"></span></span> Clone a Voice</div>
<div class="nav-item" data-nav-section="s-design" onclick="navTo('s-design')"> <span class="nav-icon"><span class="mdi mdi-auto-fix"></span></span> Design a Voice</div>
<div class="nav-item" data-nav-section="s-studio" onclick="navTo('s-studio')"> <span class="nav-icon"><span class="mdi mdi-earth"></span></span> Get a Voice Online</div>
<div class="nav-group-label">Speak</div>
<div class="nav-item" data-nav-section="s-tryout" onclick="navTo('s-tryout')"> <span class="nav-icon"><span class="mdi mdi-play"></span></span> Quick Play</div>
<div class="nav-item" data-nav-section="s-conversation" onclick="navTo('s-conversation')"> <span class="nav-icon"><span class="mdi mdi-forum-outline"></span></span> Conversation</div>
<div class="nav-item" data-nav-section="s-reader" onclick="navTo('s-reader')"> <span class="nav-icon"><span class="mdi mdi-book-open-page-variant-outline"></span></span> Read Aloud</div>
<div class="nav-item nav-tree-head" data-nav-section="s-rehearser" id="nav-rehearser-head" onclick="navTo('s-rehearser')">
<span class="nav-icon"><span class="mdi mdi-theater"></span></span>
<span class="nav-label">Script Rehearsal</span>
<span class="nav-chevron" id="nav-rehearser-chevron" onclick="event.stopPropagation();toggleNavTree('nav-rehearser-tree','nav-rehearser-chevron')"><span class="mdi mdi-chevron-down"></span></span>
</div>
<div class="nav-tree" id="nav-rehearser-tree">
<div class="nav-tree-item is-active" data-rehearser-phase="3" onclick="navRehearserPhase(3)"><span class="mdi mdi-theater"></span> Stage</div>
<div class="nav-tree-item" data-rehearser-phase="4" onclick="navRehearserPhase(4)"><span class="mdi mdi-check-circle-outline"></span> Summary</div>
<div class="nav-tree-item" id="nav-reh-impex" onclick="navRehearserImpEx()"><span class="mdi mdi-swap-vertical"></span> Import / Export</div>
</div>
<div class="nav-item nav-tree-head" data-nav-section="s-library" id="nav-library-head" onclick="navTo('s-library')">
<span class="nav-icon"><span class="mdi mdi-bookshelf"></span></span>
<span class="nav-label">Library</span>
<span class="nav-chevron" id="nav-library-chevron" onclick="event.stopPropagation();toggleNavTree('nav-library-tree','nav-library-chevron')"><span class="mdi mdi-chevron-down"></span></span>
</div>
<div class="nav-tree" id="nav-library-tree">
<div class="nav-tree-item is-active" data-library-view="books" onclick="navLibraryView('books')"><span class="mdi mdi-book-open-page-variant-outline"></span> Books</div>
<div class="nav-tree-item" data-library-view="plays" onclick="navLibraryView('plays')"><span class="mdi mdi-theater"></span> Theater Plays</div>
<div class="nav-tree-item" data-library-view="characters" onclick="navLibraryView('characters')"><span class="mdi mdi-account-box-multiple-outline"></span> Characters / Cast</div>
<div class="nav-tree" id="nav-actions-tree">
<div class="nav-tree-item" data-nav-section="s-clone" onclick="navTo('s-clone')"><span class="mdi mdi-microphone-variant"></span> Clone a Voice</div>
<div class="nav-tree-item" data-nav-section="s-design" onclick="navTo('s-design')"><span class="mdi mdi-auto-fix"></span> Design a Voice</div>
<div class="nav-tree-item" data-nav-section="s-studio" onclick="navTo('s-studio')"><span class="mdi mdi-earth"></span> Get a Voice Online</div>
</div>
<div class="nav-group-label" style="margin-top:6px">Setup</div>
<div class="nav-item" data-nav-section="s-performance" onclick="navTo('s-performance')"> <span class="nav-icon"><span class="mdi mdi-speedometer"></span></span> Benchmark</div>
<div class="nav-item nav-tree-head" data-nav-section="s-llms" id="nav-engines-head" onclick="navTo('s-llms')">
<span class="nav-icon"><span class="mdi mdi-brain"></span></span>
<span class="nav-label">Engines</span>
<span class="nav-chevron" id="nav-engines-chevron" onclick="event.stopPropagation();toggleNavTree('nav-engines-tree','nav-engines-chevron')"><span class="mdi mdi-chevron-down"></span></span>
<div class="nav-group-head" id="nav-speak-head" onclick="toggleNavTree('nav-speak-tree','nav-speak-chevron')">
<span class="nav-label">Speak</span>
<span class="nav-chevron" id="nav-speak-chevron"><span class="mdi mdi-chevron-down"></span></span>
</div>
<div class="nav-tree" id="nav-engines-tree">
<div class="nav-tree-item is-active" data-engines-cat="llm" onclick="navEnginesCat('llm')"><span class="mdi mdi-brain"></span> Language Models</div>
<div class="nav-tree-item" data-engines-cat="stt" onclick="navEnginesCat('stt')"><span class="mdi mdi-ear-hearing"></span> Speech to Text</div>
<div class="nav-tree-item" data-engines-cat="tts" onclick="navEnginesCat('tts')"><span class="mdi mdi-account-voice"></span> Text to Speech</div>
<div class="nav-tree" id="nav-speak-tree">
<div class="nav-tree-item" data-nav-section="s-tryout" onclick="navTo('s-tryout')"><span class="mdi mdi-play"></span> Quick Play</div>
<div class="nav-tree-item" data-nav-section="s-conversation" onclick="navTo('s-conversation')"><span class="mdi mdi-forum-outline"></span> Conversation</div>
<div class="nav-tree-item" data-nav-section="s-reader" onclick="navTo('s-reader')"><span class="mdi mdi-book-open-page-variant-outline"></span> Read Aloud</div>
<div class="nav-tree-head nav-subhead" data-nav-section="s-rehearser" id="nav-rehearser-head" onclick="navTo('s-rehearser')">
<span class="nav-icon"><span class="mdi mdi-theater"></span></span>
<span class="nav-label">Script Rehearsal</span>
<span class="nav-chevron" id="nav-rehearser-chevron" onclick="event.stopPropagation();toggleNavTree('nav-rehearser-tree','nav-rehearser-chevron')"><span class="mdi mdi-chevron-down"></span></span>
</div>
<div class="nav-tree" id="nav-rehearser-tree">
<div class="nav-tree-item is-active" data-rehearser-phase="3" onclick="navRehearserPhase(3)"><span class="mdi mdi-theater"></span> Stage</div>
<div class="nav-tree-item" data-rehearser-phase="4" onclick="navRehearserPhase(4)"><span class="mdi mdi-check-circle-outline"></span> Summary</div>
<div class="nav-tree-item" id="nav-reh-impex" onclick="navRehearserImpEx()"><span class="mdi mdi-swap-vertical"></span> Import / Export</div>
</div>
<div class="nav-tree-head nav-subhead" data-nav-section="s-library" id="nav-library-head" onclick="navTo('s-library')">
<span class="nav-icon"><span class="mdi mdi-bookshelf"></span></span>
<span class="nav-label">Library</span>
<span class="nav-chevron" id="nav-library-chevron" onclick="event.stopPropagation();toggleNavTree('nav-library-tree','nav-library-chevron')"><span class="mdi mdi-chevron-down"></span></span>
</div>
<div class="nav-tree" id="nav-library-tree">
<div class="nav-tree-item is-active" data-library-view="books" onclick="navLibraryView('books')"><span class="mdi mdi-book-open-page-variant-outline"></span> Books</div>
<div class="nav-tree-item" data-library-view="plays" onclick="navLibraryView('plays')"><span class="mdi mdi-theater"></span> Theater Plays</div>
<div class="nav-tree-item" data-library-view="characters" onclick="navLibraryView('characters')"><span class="mdi mdi-account-box-multiple-outline"></span> Characters / Cast</div>
</div>
</div>
<div class="nav-group-label">Integrations</div>
<div class="nav-item" data-nav-section="s-routing" onclick="navTo('s-routing')"> <span class="nav-icon"><span class="mdi mdi-swap-horizontal"></span></span> App Routing</div>
<div class="nav-item" data-nav-section="s-connect" onclick="navTo('s-connect')"> <span class="nav-icon"><span class="mdi mdi-api"></span></span> Connect Apps</div>
<div class="nav-item nav-tree-head" data-nav-section="s-settings" id="nav-settings-head" onclick="navTo('s-settings')">
<span class="nav-icon"><span class="mdi mdi-cog-outline"></span></span>
<span class="nav-label">Settings</span>
<span class="nav-chevron" id="nav-settings-chevron" onclick="event.stopPropagation();toggleNavTree('nav-settings-tree','nav-settings-chevron')"><span class="mdi mdi-chevron-down"></span></span>
<div class="nav-group-head" id="nav-setup-head" onclick="toggleNavTree('nav-setup-tree','nav-setup-chevron')">
<span class="nav-label">Setup</span>
<span class="nav-chevron" id="nav-setup-chevron"><span class="mdi mdi-chevron-down"></span></span>
</div>
<div class="nav-tree" id="nav-settings-tree">
<div class="nav-tree-item" data-settings-cat="general" onclick="navSettingsCat('general')"><span class="mdi mdi-tune-vertical"></span> General</div>
<div class="nav-tree-item" data-settings-cat="payloads" onclick="navSettingsCat('payloads')"><span class="mdi mdi-code-json"></span> Payloads</div>
<div class="nav-tree-item" data-settings-cat="storage" onclick="navSettingsCat('storage')"><span class="mdi mdi-folder-outline"></span> Storage</div>
<div class="nav-tree-item" data-settings-cat="apikeys" onclick="navSettingsCat('apikeys')"><span class="mdi mdi-key-outline"></span> API Keys</div>
<div class="nav-tree-item" data-settings-cat="backup" onclick="navSettingsCat('backup')"><span class="mdi mdi-backup-restore"></span> Backup</div>
<div class="nav-tree-item" data-settings-cat="logs" onclick="navSettingsCat('logs')"><span class="mdi mdi-text-box-outline"></span> Logs</div>
<div class="nav-tree-item" data-settings-cat="about" onclick="navSettingsCat('about')"><span class="mdi mdi-information-outline"></span> About</div>
<div class="nav-tree" id="nav-setup-tree">
<div class="nav-tree-item" data-nav-section="s-performance" onclick="navTo('s-performance')"><span class="mdi mdi-speedometer"></span> Benchmark</div>
<div class="nav-tree-head nav-subhead" data-nav-section="s-llms" id="nav-engines-head" onclick="navTo('s-llms')">
<span class="nav-icon"><span class="mdi mdi-brain"></span></span>
<span class="nav-label">Engines</span>
<span class="nav-chevron" id="nav-engines-chevron" onclick="event.stopPropagation();toggleNavTree('nav-engines-tree','nav-engines-chevron')"><span class="mdi mdi-chevron-down"></span></span>
</div>
<div class="nav-tree" id="nav-engines-tree">
<div class="nav-tree-item is-active" data-engines-cat="llm" onclick="navEnginesCat('llm')"><span class="mdi mdi-brain"></span> Language Models</div>
<div class="nav-tree-item" data-engines-cat="stt" onclick="navEnginesCat('stt')"><span class="mdi mdi-ear-hearing"></span> Speech to Text</div>
<div class="nav-tree-item" data-engines-cat="tts" onclick="navEnginesCat('tts')"><span class="mdi mdi-account-voice"></span> Text to Speech</div>
</div>
<div class="nav-tree-head nav-subhead" id="nav-integrations-head" onclick="toggleNavTree('nav-integrations-tree','nav-integrations-chevron')">
<span class="nav-icon"><span class="mdi mdi-transit-connection-variant"></span></span>
<span class="nav-label">Integrations</span>
<span class="nav-chevron" id="nav-integrations-chevron"><span class="mdi mdi-chevron-down"></span></span>
</div>
<div class="nav-tree" id="nav-integrations-tree">
<div class="nav-tree-item" data-nav-section="s-routing" onclick="navTo('s-routing')"><span class="mdi mdi-swap-horizontal"></span> App Routing</div>
<div class="nav-tree-item" data-nav-section="s-connect" onclick="navTo('s-connect')"><span class="mdi mdi-api"></span> Connect Apps</div>
</div>
<div class="nav-tree-head nav-subhead" data-nav-section="s-settings" id="nav-settings-head" onclick="navTo('s-settings')">
<span class="nav-icon"><span class="mdi mdi-cog-outline"></span></span>
<span class="nav-label">Settings</span>
<span class="nav-chevron" id="nav-settings-chevron" onclick="event.stopPropagation();toggleNavTree('nav-settings-tree','nav-settings-chevron')"><span class="mdi mdi-chevron-down"></span></span>
</div>
<div class="nav-tree" id="nav-settings-tree">
<div class="nav-tree-item" data-settings-cat="general" onclick="navSettingsCat('general')"><span class="mdi mdi-tune-vertical"></span> General</div>
<div class="nav-tree-item" data-settings-cat="payloads" onclick="navSettingsCat('payloads')"><span class="mdi mdi-code-json"></span> Payloads</div>
<div class="nav-tree-item" data-settings-cat="storage" onclick="navSettingsCat('storage')"><span class="mdi mdi-folder-outline"></span> Storage</div>
<div class="nav-tree-item" data-settings-cat="apikeys" onclick="navSettingsCat('apikeys')"><span class="mdi mdi-key-outline"></span> API Keys</div>
<div class="nav-tree-item" data-settings-cat="backup" onclick="navSettingsCat('backup')"><span class="mdi mdi-backup-restore"></span> Backup</div>
<div class="nav-tree-item" data-settings-cat="logs" onclick="navSettingsCat('logs')"><span class="mdi mdi-text-box-outline"></span> Logs</div>
<div class="nav-tree-item" data-settings-cat="about" onclick="navSettingsCat('about')"><span class="mdi mdi-information-outline"></span> About</div>
</div>
</div>
</nav>

View File

@ -69,15 +69,40 @@
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', section: 's-voices' },
{ tree: 'nav-engines-tree', chevron: 'nav-engines-chevron', section: 's-llms' },
{ tree: 'nav-settings-tree', chevron: 'nav-settings-chevron', section: 's-settings' },
{ tree: 'nav-rehearser-tree', chevron: 'nav-rehearser-chevron', section: 's-rehearser' },
{ tree: 'nav-library-tree', chevron: 'nav-library-chevron', section: 's-library' },
{ 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-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];
@ -110,13 +135,9 @@
item.classList.toggle('active', item.dataset.navSection === sectionId);
});
// Navigating to a section auto-opens its tree (but never closes others)
TREES.forEach(function (t) {
if (t.section === sectionId && !_treeOpen[t.tree]) {
_treeOpen[t.tree] = true;
localStorage.setItem('ttsvc_trees', JSON.stringify(_treeOpen));
}
});
// 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)

View File

@ -142,6 +142,36 @@ body {
.nav-tree-item.nav-tree-tag { padding-left: 38px; }
.nav-tree-item.nav-tree-tag .mdi { font-size: 13px; opacity: .8; }
.ntc { font-size: 14px; color: var(--subtext); font-weight: 500; }
/* ── Collapsible group headers (Voice Actions · Speak · Setup) ───────────── */
.nav-group-head {
display: flex; align-items: center; gap: 8px; cursor: pointer;
padding: 10px 16px 4px; margin-top: 2px;
font-size: 10px; font-weight: 800; color: var(--subtext);
text-transform: uppercase; letter-spacing: 0.09em;
user-select: none;
}
.nav-group-head .nav-label { flex: 1; }
.nav-group-head:hover { color: var(--text); }
.nav-group-head .nav-chevron { font-size: 13px; }
/* Sub-headers nested inside a tree (Tags · Script Rehearsal · Library ·
Engines · Integrations · Settings) look like items, carry a chevron */
.nav-tree .nav-tree-head.nav-subhead {
display: flex; align-items: center; gap: 7px; cursor: pointer;
padding: 5px 16px 5px 32px; font-size: 12.5px; color: var(--subtext);
transition: background .1s, color .1s;
}
.nav-tree .nav-tree-head.nav-subhead .nav-icon .mdi,
.nav-tree .nav-tree-head.nav-subhead > .mdi { font-size: 14px; flex-shrink: 0; }
.nav-tree .nav-tree-head.nav-subhead .nav-label { flex: 1; }
.nav-tree .nav-tree-head.nav-subhead:hover { background: var(--panel); color: var(--text); }
.nav-tree .nav-tree-head.nav-subhead.active { color: var(--accent); font-weight: 700; }
/* Nested trees indent one extra level per depth */
.nav-tree .nav-tree .nav-tree-item,
.nav-tree .nav-tree .nav-tree-head.nav-subhead { padding-left: 44px; }
.nav-tree .nav-tree .nav-tree .nav-tree-item { padding-left: 56px; }
.nav-tree-item.is-active .ntc { color: var(--accent); }
.nav-item {