polish(ui): sidebar nav a11y + scroll/focus refinements (nav slice 3)
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 45s
CI / integration (push) Successful in 2m17s
CI / publish (push) Successful in 59s

- Only the nav list scrolls when long (min-height:0 + overflow on .side-nav);
  the brand and user/logout stay pinned top/bottom.
- Keyboard focus rings (:focus-visible) on every interactive sidebar element
  (brand, nav links, user/logout) and the mobile ☰ toggle.
- Mobile toggle is now a real toggle: aria-controls/aria-expanded kept in sync,
  Escape closes the off-canvas sidebar, scrim close stays in sync too.
- prefers-reduced-motion disables the sidebar slide + link transitions.
- Confirmed no dead top-nav CSS/markup remnants from the old top bar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
This commit is contained in:
2026-06-20 19:14:49 -04:00
parent 28c9a4dd2f
commit a35c369dd4
+31 -6
View File
@@ -57,7 +57,7 @@ code { font-family: ui-monospace, monospace; font-size: 0.875em; background: var
width: 220px; flex-shrink: 0; background: var(--bg-card);
border-right: 1px solid var(--border-mid);
display: flex; flex-direction: column;
position: sticky; top: 0; height: 100vh; overflow-y: auto; z-index: 20;
position: sticky; top: 0; height: 100vh; z-index: 20;
}
.sidebar .brand {
font-family: var(--font-serif); font-weight: 700; font-size: 1.15rem;
@@ -66,7 +66,8 @@ code { font-family: ui-monospace, monospace; font-size: 0.875em; background: var
padding: 0.85rem 1.25rem; border-bottom: 1px solid var(--border);
}
.sidebar .brand svg { flex-shrink: 0; }
.side-nav { flex: 1; padding: 0.75rem 0; }
/* Only the nav list scrolls when long — the brand and user/logout stay pinned. */
.side-nav { flex: 1; min-height: 0; overflow-y: auto; padding: 0.75rem 0; }
.nav-group { margin-bottom: 0.9rem; }
.nav-group-label {
font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.09em;
@@ -89,6 +90,12 @@ code { font-family: ui-monospace, monospace; font-size: 0.875em; background: var
display: flex; align-items: center; gap: 0.4rem;
}
.side-user a:hover { color: var(--text); }
/* Keyboard focus: a clear inset ring on every interactive sidebar element. */
.sidebar .brand:focus-visible,
.side-nav a:focus-visible,
.side-user a:focus-visible {
outline: 2px solid var(--accent); outline-offset: -2px; color: var(--text);
}
.app-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.topbar { display: none; } /* slim mobile bar; shown only under 900px */
.nav-scrim { display: none; }
@@ -111,8 +118,10 @@ main { padding: 1.5rem 2rem; max-width: 1400px; margin: 0 auto; width: 100%; box
}
.sidebar-toggle {
background: none; border: none; color: var(--text-muted); font-size: 1.3rem;
cursor: pointer; line-height: 1; padding: 0.25rem;
cursor: pointer; line-height: 1; padding: 0.25rem; border-radius: 4px;
}
.sidebar-toggle:hover { color: var(--text); }
.sidebar-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.topbar .brand-sm {
font-family: var(--font-serif); color: var(--gold); font-weight: 700;
font-size: 1.05rem; text-decoration: none;
@@ -122,6 +131,11 @@ main { padding: 1.5rem 2rem; max-width: 1400px; margin: 0 auto; width: 100%; box
}
}
@media (prefers-reduced-motion: reduce) {
.sidebar { transition: none; }
.side-nav a { transition: none; }
}
/* Alerts */
.alert { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
.alert-error { background: var(--red-dim); border: 1px solid #6a1820; color: #ffb0b8; }
@@ -266,7 +280,7 @@ body.dash-editing .widget-drawer { transform:translateY(0); }
{% set _p = request.path %}
<div class="app-shell">
{% if session.user_id is defined %}
<aside class="sidebar">
<aside class="sidebar" id="sidebar">
<a href="/" class="brand">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<circle cx="12" cy="12" r="10.5" fill="none" stroke="#c8a840" stroke-width="1.5"/>
@@ -316,7 +330,8 @@ body.dash-editing .widget-drawer { transform:translateY(0); }
<div class="app-main">
{% if session.user_id is defined %}
<div class="topbar">
<button class="sidebar-toggle" onclick="document.body.classList.toggle('nav-open')" aria-label="Toggle navigation"></button>
<button class="sidebar-toggle" type="button" aria-label="Toggle navigation"
aria-controls="sidebar" aria-expanded="false" onclick="toggleNav(this)"></button>
<a href="/" class="brand-sm">Steward</a>
</div>
{% endif %}
@@ -333,6 +348,16 @@ function setTimeRange(val) {
history.replaceState({}, '', url);
document.body.dispatchEvent(new CustomEvent('rangeChange'));
}
// Mobile sidebar (off-canvas). Keep aria-expanded in sync; Escape closes it.
function setNavOpen(open) {
document.body.classList.toggle('nav-open', open);
var btn = document.querySelector('.sidebar-toggle');
if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
}
function toggleNav(btn) { setNavOpen(!document.body.classList.contains('nav-open')); }
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && document.body.classList.contains('nav-open')) setNavOpen(false);
});
</script>
<main>
@@ -387,7 +412,7 @@ function setTimeRange(val) {
</div>{# .app-main #}
</div>{# .app-shell #}
{% if session.user_id is defined %}
<div class="nav-scrim" onclick="document.body.classList.remove('nav-open')"></div>
<div class="nav-scrim" onclick="setNavOpen(false)"></div>
{% endif %}
{% block extra_scripts %}{% endblock %}