feat(admin): flat sidebar + horizontal subnav navigation

Replace grouped section labels + 9 individual links with 5 flat
section-level items (Dashboard, Manage, Content, Engagement, System)
and a horizontal tab strip for multi-page sections. Active state
derived via _section_map dict — no JS required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-27 07:53:07 +01:00
parent 2b2a7274ca
commit 7c5235ff39

View File

@@ -88,17 +88,6 @@
flex: 1;
padding: 12px 10px;
}
.admin-nav-section {
margin-bottom: 6px;
}
.admin-nav-label {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(255,255,255,0.3);
padding: 10px 10px 4px;
}
.admin-nav-item {
display: flex;
@@ -112,6 +101,7 @@
text-decoration: none;
transition: background 0.12s, color 0.12s;
position: relative;
margin-bottom: 1px;
}
.admin-nav-item:hover {
background: rgba(255,255,255,0.07);
@@ -155,6 +145,12 @@
}
.admin-nav-badge.unread { background: #D97706; }
.admin-nav-divider {
height: 1px;
background: rgba(255,255,255,0.07);
margin: 6px 10px;
}
.admin-sidebar-footer {
padding: 12px 10px;
border-top: 1px solid rgba(255,255,255,0.08);
@@ -194,6 +190,28 @@
color: #78716C;
}
/* ── Horizontal subnav ── */
.admin-subnav {
display: flex; align-items: stretch; padding: 0 28px;
background: #fff; border-bottom: 1px solid #E8DFD5;
flex-shrink: 0; overflow-x: auto; gap: 0;
}
.admin-subnav a {
display: flex; align-items: center; gap: 5px;
padding: 0 1px; margin: 0 14px 0 0; height: 40px;
font-size: 13px; font-weight: 500; color: #78716C;
text-decoration: none; white-space: nowrap;
border-bottom: 2px solid transparent; margin-bottom: -1px;
transition: color 0.12s, border-color 0.12s;
}
.admin-subnav a:hover { color: #B45309; text-decoration: none; }
.admin-subnav a.active { color: #B45309; border-bottom-color: #B45309; font-weight: 600; }
.admin-subnav__badge {
font-size: 9px; padding: 1px 5px; border-radius: 9999px; font-weight: 700;
}
.admin-subnav__badge.error { background: #EF4444; color: white; }
.admin-subnav__badge.unread { background: #D97706; color: white; }
.admin-content {
flex: 1;
padding: 28px;
@@ -219,6 +237,7 @@
.admin-main { margin-left: 0; }
.admin-mobile-toggle { display: flex; }
.admin-content { padding: 16px; }
.admin-subnav { padding: 0 16px; }
}
/* ── Overlay ── */
@@ -247,6 +266,14 @@
{% block admin_head %}{% endblock %}
</head>
<body>
{%- set _section_map = {
'dashboard': 'overview',
'users': 'manage', 'tasks': 'manage',
'cms': 'content', 'cms_templates': 'content',
'feedback': 'engagement', 'waitlist': 'engagement',
'flags': 'system', 'emails': 'system',
} -%}
{%- set active_section = _section_map.get(admin_page|default(''), 'overview') -%}
<div class="admin-shell">
<!-- Sidebar -->
@@ -260,107 +287,64 @@
<!-- Navigation -->
<nav class="admin-nav">
<div class="admin-nav-section">
<div class="admin-nav-label">Overview</div>
<a href="{{ url_for('admin.index') }}"
class="admin-nav-item {% if admin_page == 'dashboard' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>
</svg>
Dashboard
</a>
</div>
<div class="admin-nav-section">
<div class="admin-nav-label">Manage</div>
<a href="{{ url_for('admin.users') }}"
class="admin-nav-item {% if admin_page == 'users' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
Users
</a>
<a href="{{ url_for('admin.tasks') }}"
class="admin-nav-item {% if admin_page == 'tasks' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<polyline points="9 11 12 14 22 4"/>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
</svg>
Tasks
{% if stats is defined and stats.tasks_failed > 0 %}
<span class="admin-nav-badge">{{ stats.tasks_failed }}</span>
{% endif %}
</a>
</div>
<a href="{{ url_for('admin.index') }}"
class="admin-nav-item {% if active_section == 'overview' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>
</svg>
Dashboard
</a>
<div class="admin-nav-section">
<div class="admin-nav-label">Engagement</div>
<a href="{{ url_for('admin.feedback') }}"
class="admin-nav-item {% if admin_page == 'feedback' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Feedback
{% if feedback_unread is defined and feedback_unread > 0 %}
<span class="admin-nav-badge unread">{{ feedback_unread }}</span>
{% endif %}
</a>
<a href="{{ url_for('admin.waitlist') }}"
class="admin-nav-item {% if admin_page == 'waitlist' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/>
</svg>
Waitlist
</a>
</div>
<div class="admin-nav-divider"></div>
<div class="admin-nav-section">
<div class="admin-nav-label">Content</div>
<a href="{{ url_for('cms.cms_index') }}"
class="admin-nav-item {% if admin_page == 'cms' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
Articles
</a>
<a href="{{ url_for('cms.template_list') }}"
class="admin-nav-item {% if admin_page == 'cms_templates' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/>
</svg>
pSEO Templates
</a>
</div>
<a href="{{ url_for('admin.users') }}"
class="admin-nav-item {% if active_section == 'manage' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
Manage
{% if stats is defined and stats.tasks_failed > 0 %}
<span class="admin-nav-badge">{{ stats.tasks_failed }}</span>
{% endif %}
</a>
<a href="{{ url_for('cms.cms_index') }}"
class="admin-nav-item {% if active_section == 'content' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
Content
</a>
<div class="admin-nav-divider"></div>
<a href="{{ url_for('admin.feedback') }}"
class="admin-nav-item {% if active_section == 'engagement' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Engagement
{% if feedback_unread is defined and feedback_unread > 0 %}
<span class="admin-nav-badge unread">{{ feedback_unread }}</span>
{% endif %}
</a>
<a href="{{ url_for('admin.flags') }}"
class="admin-nav-item {% if active_section == 'system' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
System
</a>
<div class="admin-nav-section">
<div class="admin-nav-label">System</div>
<a href="{{ url_for('admin.flags') }}"
class="admin-nav-item {% if admin_page == 'flags' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/>
<line x1="4" y1="22" x2="4" y2="15"/>
</svg>
Feature Flags
</a>
<a href="{{ url_for('admin.emails') }}"
class="admin-nav-item {% if admin_page == 'emails' %}active{% endif %}">
<svg class="admin-nav-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
Email Log
</a>
</div>
</nav>
<!-- Footer links -->
@@ -414,6 +398,39 @@
</div>
</header>
{# ── Horizontal subnav — rendered only for multi-page sections ── #}
{% if active_section == 'manage' %}
<nav class="admin-subnav">
<a href="{{ url_for('admin.users') }}" class="{% if admin_page == 'users' %}active{% endif %}">Users</a>
<a href="{{ url_for('admin.tasks') }}" class="{% if admin_page == 'tasks' %}active{% endif %}">
Tasks
{% if stats is defined and stats.tasks_failed > 0 %}
<span class="admin-subnav__badge error">{{ stats.tasks_failed }}</span>
{% endif %}
</a>
</nav>
{% elif active_section == 'content' %}
<nav class="admin-subnav">
<a href="{{ url_for('cms.cms_index') }}" class="{% if admin_page == 'cms' %}active{% endif %}">Articles</a>
<a href="{{ url_for('cms.template_list') }}" class="{% if admin_page == 'cms_templates' %}active{% endif %}">pSEO Templates</a>
</nav>
{% elif active_section == 'engagement' %}
<nav class="admin-subnav">
<a href="{{ url_for('admin.feedback') }}" class="{% if admin_page == 'feedback' %}active{% endif %}">
Feedback
{% if feedback_unread is defined and feedback_unread > 0 %}
<span class="admin-subnav__badge unread">{{ feedback_unread }}</span>
{% endif %}
</a>
<a href="{{ url_for('admin.waitlist') }}" class="{% if admin_page == 'waitlist' %}active{% endif %}">Waitlist</a>
</nav>
{% elif active_section == 'system' %}
<nav class="admin-subnav">
<a href="{{ url_for('admin.flags') }}" class="{% if admin_page == 'flags' %}active{% endif %}">Feature Flags</a>
<a href="{{ url_for('admin.emails') }}" class="{% if admin_page == 'emails' %}active{% endif %}">Email Log</a>
</nav>
{% endif %}
<!-- Impersonation banner -->
{% if session.get('admin_impersonating') %}
<div class="impersonate-banner">