Make the control panel a real app shell with a fixed sidebar
The navbar and the menu stay put; only the content scrolls. That is a property of the layout, not a `sticky` class: the body is a flex column pinned to the viewport with overflow hidden, and <main> is the single scrolling region. The sidebar is a SIBLING of <main>, not inside it, so it sits outside that region and cannot drift by construction. A sticky sidebar in a scrollable body still slides on a long page, which is the failure this avoids. The tabs become a daisyUI menu with an active indicator (`menu-active`) and the hover/focus states `.menu` already provides. Sidebar below `lg` would be a menu nobody can reach, so the same items render as a horizontal menu there -- from ONE partial, because two copies of a link list is how a new section ends up reachable on a desktop and invisible on a phone. Auth pages leave the menu block empty and <main> simply takes the full width. Verified in a browser: content scrolled 800px, sidebar and navbar moved 0px, and the body is not scrollable at all. Below `lg` the sidebar hides and all five links are still there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
37
controlpanel/templates/controlpanel/_nav_items.html
Normal file
37
controlpanel/templates/controlpanel/_nav_items.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% load lucide %}
|
||||
|
||||
{% comment %}
|
||||
The panel's navigation, in one place: the sidebar renders it on a wide screen and the
|
||||
collapsed menu renders it on a narrow one. Two copies of a link list is how a new section
|
||||
ends up reachable on a desktop and invisible on a phone.
|
||||
|
||||
`menu-active` is daisyUI 5's active state; hover and focus come with `.menu` itself.
|
||||
{% endcomment %}
|
||||
<li>
|
||||
<a class="{% if nav == 'dashboard' %}menu-active{% endif %}" href="{% url 'controlpanel:dashboard' %}">
|
||||
{% lucide "layout-dashboard" size=16 %} Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="{% if nav == 'clubs' %}menu-active{% endif %}" href="{% url 'controlpanel:club_list' %}">
|
||||
{% lucide "building-2" size=16 %} Clubs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="{% if nav == 'billing' %}menu-active{% endif %}" href="{% url 'controlpanel:billing' %}">
|
||||
{% lucide "receipt-euro" size=16 %} Billing
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="{% if nav == 'features' %}menu-active{% endif %}" href="{% url 'controlpanel:features' %}">
|
||||
{% lucide "toggle-right" size=16 %} Features
|
||||
</a>
|
||||
</li>
|
||||
{% if user.is_superuser %}
|
||||
{# Superusers only, exactly as the view is gated: a link staff cannot follow is a lie. #}
|
||||
<li>
|
||||
<a class="{% if nav == 'admins' %}menu-active{% endif %}" href="{% url 'controlpanel:admins' %}">
|
||||
{% lucide "user-cog" size=16 %} Admins
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
@@ -5,15 +5,28 @@
|
||||
{% block panel_title %}Control panel{% endblock panel_title %} · RosterChief
|
||||
{% endblock title %}
|
||||
|
||||
{% block menu %}
|
||||
{% comment %}
|
||||
Outside <main>, so it never scrolls with the content. Its own overflow-y-auto is for
|
||||
the day the menu itself grows taller than the screen.
|
||||
{% endcomment %}
|
||||
<aside class="hidden w-64 shrink-0 overflow-y-auto border-r border-base-300 bg-base-100 lg:block">
|
||||
<ul class="menu w-full gap-1 p-3">
|
||||
<li class="menu-title">Platform</li>
|
||||
{% include "controlpanel/_nav_items.html" %}
|
||||
</ul>
|
||||
</aside>
|
||||
{% endblock menu %}
|
||||
|
||||
{% block main %}
|
||||
{% if maintenance_on %}
|
||||
<div class="alert alert-error mb-6">
|
||||
{% lucide "wrench" size=20 %}
|
||||
<span>
|
||||
<strong>The platform is closed for maintenance.</strong>
|
||||
Clubs see a maintenance page and the scheduled jobs are standing down.
|
||||
</span>
|
||||
<a class="btn btn-sm" href="{% url 'controlpanel:features' %}">Reopen</a>
|
||||
<strong>The platform is currently closed for maintenance.</strong>
|
||||
Clubs see a maintenance page and the scheduled jobs are standing down.
|
||||
</span>
|
||||
<a class="btn btn-sm gap-2 btn-error btn-soft" href="{% url 'controlpanel:features' %}">{% lucide "unlock" size=16 %} Reopen platform</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mb-6 flex flex-wrap items-center justify-between gap-3">
|
||||
@@ -28,15 +41,11 @@
|
||||
{% block actions %}{% endblock actions %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role="tablist" class="tabs-boxed tabs mb-6 w-fit">
|
||||
<a role="tab" href="{% url 'controlpanel:dashboard' %}" class="tab gap-2 {% if nav == 'dashboard' %}tab-active{% endif %}">{% lucide "layout-dashboard" size=16 %} Dashboard</a>
|
||||
<a role="tab" href="{% url 'controlpanel:club_list' %}" class="tab gap-2 {% if nav == 'clubs' %}tab-active{% endif %}">{% lucide "building-2" size=16 %} Clubs</a>
|
||||
<a role="tab" href="{% url 'controlpanel:billing' %}" class="tab gap-2 {% if nav == 'billing' %}tab-active{% endif %}">{% lucide "receipt-euro" size=16 %} Billing</a>
|
||||
<a role="tab" href="{% url 'controlpanel:features' %}" class="tab gap-2 {% if nav == 'features' %}tab-active{% endif %}">{% lucide "toggle-right" size=16 %} Features</a>
|
||||
{% if user.is_superuser %}
|
||||
<a role="tab" href="{% url 'controlpanel:admins' %}" class="tab gap-2 {% if nav == 'admins' %}tab-active{% endif %}">{% lucide "user-cog" size=16 %} Admins</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Below `lg` the sidebar is hidden, so the same links appear here rather than nowhere. #}
|
||||
<ul class="menu menu-horizontal mb-6 w-full gap-1 overflow-x-auto rounded-box bg-base-100 lg:hidden">
|
||||
{% include "controlpanel/_nav_items.html" %}
|
||||
</ul>
|
||||
|
||||
{% block panel %}{% endblock panel %}
|
||||
{% endblock main %}
|
||||
|
||||
5026
static/css/app.css
5026
static/css/app.css
File diff suppressed because one or more lines are too long
@@ -33,8 +33,14 @@
|
||||
{% block extra %}{% endblock extra %}
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen bg-base-200">
|
||||
<div class="navbar mb-4 border-b border-base-300 bg-base-100 px-6 shadow-sm">
|
||||
{% comment %}
|
||||
An app shell: the viewport is the frame, and exactly one region scrolls. The body is a
|
||||
flex column pinned to the screen height with overflow hidden, so the navbar and the
|
||||
sidebar cannot be scrolled away — only <main> moves. Leave the body scrollable instead
|
||||
and a "sticky" sidebar still drifts on a long page, which is the thing this is for.
|
||||
{% endcomment %}
|
||||
<body class="flex h-screen flex-col overflow-hidden bg-base-200">
|
||||
<div class="navbar shrink-0 border-b border-base-300 bg-base-100 px-6 shadow-sm">
|
||||
<div class="my-4 flex-1">
|
||||
{% block brand %}{% endblock brand %}
|
||||
</div>
|
||||
@@ -65,25 +71,36 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if messages %}
|
||||
<div class="mx-auto mt-4 w-full space-y-2 px-4">
|
||||
{% for message in messages %}
|
||||
{% with alert=message|as_alert %}
|
||||
<div class="alert alert-soft {{ alert.css }}" role="alert">
|
||||
{% lucide alert.icon size=20 %}
|
||||
<div>
|
||||
<div class="font-bold">{{ alert.title }}</div>
|
||||
<div class="text-sm">{{ alert.body }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
{% comment %}
|
||||
The sidebar is a sibling of <main>, not inside it, so it sits outside the one
|
||||
scrolling region and stays put by construction. Pages with no menu (the auth
|
||||
screens) leave the block empty and <main> simply takes the full width.
|
||||
{% endcomment %}
|
||||
{% block menu %}{% endblock menu %}
|
||||
|
||||
<main class="mx-auto w-full py-4 px-8">
|
||||
{% block main %}{% endblock main %}
|
||||
</main>
|
||||
<main class="flex-1 overflow-y-auto">
|
||||
<div class="mx-auto w-full px-8 py-6">
|
||||
{% if messages %}
|
||||
<div class="mb-6 w-full space-y-2">
|
||||
{% for message in messages %}
|
||||
{% with alert=message|as_alert %}
|
||||
<div class="alert alert-soft {{ alert.css }}" role="alert">
|
||||
{% lucide alert.icon size=20 %}
|
||||
<div>
|
||||
<div class="font-bold">{{ alert.title }}</div>
|
||||
<div class="text-sm">{{ alert.body }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block main %}{% endblock main %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// The button cycles light -> dark -> auto. "auto" removes the attribute and the
|
||||
|
||||
Reference in New Issue
Block a user