-
General
-
-
-
Payloads
-
Storage
-
API Keys
-
Backup
-
Logs
-
About
+
+
Benchmark
+
+
+ Engines
+
+
+
+
Language Models
+
Speech to Text
+
Text to Speech
+
+
+
+ Integrations
+
+
+
+
App Routing
+
Connect Apps
+
+
+
+ Settings
+
+
+
+
General
+
Payloads
+
Storage
+
API Keys
+
Backup
+
Logs
+
About
+
diff --git a/static/nav.js b/static/nav.js
index 8657fa4..6bae51c 100644
--- a/static/nav.js
+++ b/static/nav.js
@@ -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)
diff --git a/static/style.css b/static/style.css
index 620afce..6399554 100644
--- a/static/style.css
+++ b/static/style.css
@@ -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 {