Settings: left sidebar nav with separate page views per category

Replaced horizontal tab bar with a left sidebar navigation
that feels like separate pages:
- Left nav with icons + labels (Connection, Audio, 4CM Routing,
  Network, Bluetooth, About)
- Active category highlighted in accent color
- Each category has its own URL via hash (#connection, #audio, etc.)
- Back/forward browser navigation via popstate handler
- Stacked layout on mobile (sidebar becomes scrollable row)
This commit is contained in:
2026-06-16 18:19:32 -04:00
parent 29542e196b
commit 23a83cbd7b
2 changed files with 392 additions and 263 deletions
+95 -24
View File
@@ -696,43 +696,87 @@ body {
line-height: 1.4;
}
/* ── Settings tabs ────────────────────────────────────────────────── */
/* ── Settings layout: sidebar + content ──────────────────────────── */
.settings-tabs {
.settings-layout {
display: flex;
gap: 4px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
margin-bottom: 12px;
padding-bottom: 4px;
border-bottom: 1px solid var(--border);
gap: 16px;
align-items: flex-start;
}
.settings-tabs::-webkit-scrollbar {
display: none;
/* ── Settings sidebar ──────────────────────────────────────────────── */
.settings-sidebar {
width: 180px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 2px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 6px;
position: sticky;
top: 12px;
}
.settings-tab {
padding: 8px 14px;
.settings-nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 6px;
text-decoration: none;
color: var(--text-secondary);
font-size: 0.85rem;
font-weight: 500;
transition: all 0.15s;
cursor: pointer;
touch-action: manipulation;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 0.82rem;
cursor: pointer;
white-space: nowrap;
border-bottom: 2px solid transparent;
transition: color 0.2s, border-color 0.2s;
touch-action: manipulation;
width: 100%;
text-align: left;
}
.settings-tab:hover {
.settings-nav-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.settings-tab.active {
color: var(--accent);
border-bottom-color: var(--accent);
.settings-nav-item.active {
background: var(--accent);
color: #000;
font-weight: 600;
}
.settings-nav-item.active .settings-nav-icon {
filter: none;
}
.settings-nav-icon {
font-size: 1rem;
width: 22px;
text-align: center;
flex-shrink: 0;
opacity: 0.7;
}
.settings-nav-item.active .settings-nav-icon {
opacity: 1;
}
.settings-nav-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── Settings content panel ──────────────────────────────────────────── */
.settings-content {
flex: 1;
min-width: 0;
}
.settings-panel {
@@ -743,6 +787,33 @@ body {
display: block;
}
/* Mobile: stack sidebar above content */
@media (max-width: 640px) {
.settings-layout {
flex-direction: column;
}
.settings-sidebar {
width: 100%;
flex-direction: row;
overflow-x: auto;
position: static;
scrollbar-width: none;
}
.settings-sidebar::-webkit-scrollbar {
display: none;
}
.settings-nav-item {
white-space: nowrap;
flex-shrink: 0;
padding: 8px 12px;
font-size: 0.8rem;
}
.settings-nav-icon {
font-size: 0.85rem;
width: 18px;
}
}
/* ── Network UI ──────────────────────────────────────────────────── */
.network-status-row {
+73 -15
View File
@@ -6,16 +6,37 @@
<h1>Settings</h1>
</div>
<!-- Tab navigation -->
<div class="settings-tabs">
<button class="settings-tab active" data-tab="connection" onclick="switchTab('connection')">Connection</button>
<button class="settings-tab" data-tab="audio" onclick="switchTab('audio')">Audio</button>
<button class="settings-tab" data-tab="routing" onclick="switchTab('routing')">4CM Routing</button>
<button class="settings-tab" data-tab="network" onclick="switchTab('network')">Network</button>
<button class="settings-tab" data-tab="bluetooth" onclick="switchTab('bluetooth')">Bluetooth</button>
<button class="settings-tab" data-tab="about" onclick="switchTab('about')">About</button>
</div>
<div class="settings-layout">
<!-- Left sidebar navigation -->
<nav class="settings-sidebar">
<a class="settings-nav-item active" href="#connection" data-tab="connection" onclick="switchTab('connection')">
<span class="settings-nav-icon">🔌</span>
<span class="settings-nav-label">Connection</span>
</a>
<a class="settings-nav-item" href="#audio" data-tab="audio" onclick="switchTab('audio')">
<span class="settings-nav-icon">🎚️</span>
<span class="settings-nav-label">Audio</span>
</a>
<a class="settings-nav-item" href="#routing" data-tab="routing" onclick="switchTab('routing')">
<span class="settings-nav-icon">🔀</span>
<span class="settings-nav-label">4CM Routing</span>
</a>
<a class="settings-nav-item" href="#network" data-tab="network" onclick="switchTab('network')">
<span class="settings-nav-icon">📶</span>
<span class="settings-nav-label">Network</span>
</a>
<a class="settings-nav-item" href="#bluetooth" data-tab="bluetooth" onclick="switchTab('bluetooth')">
<span class="settings-nav-icon">🔵</span>
<span class="settings-nav-label">Bluetooth</span>
</a>
<a class="settings-nav-item" href="#about" data-tab="about" onclick="switchTab('about')">
<span class="settings-nav-icon"></span>
<span class="settings-nav-label">About</span>
</a>
</nav>
<!-- Content area -->
<div class="settings-content">
<!-- Tab: Connection -->
<div class="settings-panel active" id="tab-connection">
<div class="card">
@@ -60,6 +81,22 @@
</button>
</div>
</div>
<div class="setting-row" style="border-bottom:none">
<div class="setting-label">Latency Profile</div>
<div class="setting-value">
<select id="audio-profile-select" class="profile-select"
onchange="switchAudioProfile(this.value)">
<option value="">Loading...</option>
</select>
</div>
</div>
<div class="setting-row" style="border-bottom:none">
<div class="setting-label">JACK Status</div>
<div class="setting-value">
<span id="jack-status" class="jack-check"></span>
<button class="btn btn-secondary btn-sm" onclick="refreshAudioProfile()"></button>
</div>
</div>
</div>
</div>
</div>
@@ -263,6 +300,8 @@
</div>
</div>
</div>
</div>
</div>
<!-- WiFi connect modal -->
<div class="modal-overlay" id="wifi-connect-modal" style="display:none;" onclick="if(event.target===this)closeWiFiModal()">
@@ -293,13 +332,21 @@
<script src="/static/network.js"></script>
<script src="/static/bluetooth.js"></script>
<script>
/* ── Tab switching ────────────────────────────────────────────────── */
/* ── Tab switching (left nav) ──────────────────────────────────── */
function switchTab(tabId) {
document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
// Update nav items
document.querySelectorAll('.settings-nav-item').forEach(a => a.classList.remove('active'));
document.querySelector(`.settings-nav-item[data-tab="${tabId}"]`).classList.add('active');
// Update panels
document.querySelectorAll('.settings-panel').forEach(p => p.classList.remove('active'));
document.querySelector(`.settings-tab[data-tab="${tabId}"]`).classList.add('active');
document.getElementById(`tab-${tabId}`).classList.add('active');
// Sync URL hash
if (window.location.hash !== `#${tabId}`) {
history.pushState(null, '', `#${tabId}`);
}
// Refresh tab data when switching to it
if (tabId === 'network') {
refreshWiFiStatus();
@@ -308,14 +355,25 @@ function switchTab(tabId) {
refreshBTStatus();
refreshBTMIDIStatus();
loadPairedDevices();
} else if (tabId === 'audio') {
refreshAudioProfile();
}
}
/* ── Initial load on settings page ───────────────────────────────── */
/* ── Initial load & hash routing ───────────────────────────────── */
document.addEventListener('DOMContentLoaded', () => {
// Check if we should auto-switch based on URL hash
const hash = window.location.hash.replace('#', '');
if (['network', 'bluetooth', 'audio', 'routing', 'about'].includes(hash)) {
const valid = ['connection', 'audio', 'routing', 'network', 'bluetooth', 'about'];
if (valid.includes(hash)) {
switchTab(hash);
}
});
/* ── Listen for back/forward navigation ───────────────────────── */
window.addEventListener('popstate', () => {
const hash = window.location.hash.replace('#', '');
const valid = ['connection', 'audio', 'routing', 'network', 'bluetooth', 'about'];
if (valid.includes(hash)) {
switchTab(hash);
}
});