{% extends "base.html" %}
{# The signed-in layout: a top bar, a foldable sidebar, and the region pages
fill. Every page behind the session guard should extend this rather than
base.html, so the navigation is inherited instead of re-declared.
The sections come from app/endpoints/shell.py NAV, already filtered to what
this caller may see and already carrying their fold state. Rendered by the
server rather than fetched: the sidebar is part of the page, and painting it
from JavaScript means every navigation shows an empty rail for a moment. #}
{% block head %}
<link rel="stylesheet" href="/static/css/shell.css">
{% endblock %}
{% block body %}
<header class="appbar">
{# The control is the button. aria-expanded is toggled by shell.js, so a
screen reader is told the state rather than left to infer it from a glyph. #}
<button class="navtoggle" id="navToggle" type="button"
aria-label="Toggle navigation" aria-controls="sidebar" aria-expanded="true">
<span></span><span></span><span></span>
</button>
<a class="appbrand" href="/">{{ app_name }}</a>
<div class="acct" id="acctMenu">
<button class="avatarbtn" id="acctBtn" type="button"
aria-haspopup="menu" aria-expanded="false" aria-label="Account"></button>
<div class="acctdrop" id="acctDrop" role="menu" hidden>
<a role="menuitem" href="/account">Security</a>
<a role="menuitem" href="/logout">Sign out</a>
</div>
</div>
</header>
<div class="shell">
{# Closes the sidebar when it is over the content on a narrow screen. Inert at
desktop width, where the sidebar is beside the content and nothing is
covered. #}
<div class="scrim" id="scrim" hidden></div>
<aside class="sidebar" id="sidebar">
<nav aria-label="Sections">
{% for section in nav_sections %}
<div class="navsec{% if section.folded %} folded{% endif %}"
data-key="{{ section.key }}">
<div class="h">
{# The twisty is the control; the heading around it is only a heading.
A control inside a control has no meaning in the accessibility tree
- a screen reader announces one thing where there are two - so the
button is the twisty, and the rest of the heading keeps its click
for the mouse alone, with no role and no tabindex duplicating it. #}
<button class="navsec-tw" type="button"
aria-label="{{ section.title }}"
aria-controls="navsec-b-{{ loop.index }}"
aria-expanded="{{ 'false' if section.folded else 'true' }}">▾</button>
<span class="navsec-t">{{ section.title }}</span>
</div>
<div class="navsec-b" id="navsec-b-{{ loop.index }}">
{% for item in section["items"] %}
<a class="navitem{% if item.href == request.path %} here{% endif %}"
href="{{ item.href }}"
{% if item.href == request.path %}aria-current="page"{% endif %}>{{ item.label }}</a>
{% endfor %}
</div>
</div>
{% endfor %}
{# The tree: the organisation's own categories, and everything nobody has
filed. Two levels, both foldable, both remembered per person. #}
<div class="navsec" data-key="sec:tree">
<div class="h">
<button class="navsec-tw" type="button" aria-label="Categories"
aria-controls="navsec-b-tree" aria-expanded="true">▾</button>
<span class="navsec-t">Categories</span>
{% if nav_tree.can_arrange %}
{# Inside the heading but outside the twisty: a control within a
control has no meaning in the accessibility tree. #}
<button class="navsec-add" type="button" id="addCategory"
aria-label="Add a category" title="Add a category">+</button>
{% endif %}
</div>
<div class="navsec-b" id="navsec-b-tree">
{% for category in nav_tree.categories %}
<div class="navcat{% if category.folded %} folded{% endif %}"
data-key="cat:{{ category.id }}" data-cat="{{ category.id }}">
<div class="navcat-h">
<button class="navcat-tw" type="button"
aria-label="{{ category.name }}"
aria-controls="navcat-b-{{ category.id }}"
aria-expanded="{{ 'false' if category.folded else 'true' }}">▾</button>
<span class="navcat-nm">{{ category.name }}</span>
<span class="navcat-n">{{ category["items"]|length }}</span>
</div>
<div class="navcat-b" id="navcat-b-{{ category.id }}">
{% for item in category["items"] %}
{% include "_navitem.html" %}
{% else %}
<p class="navcat-empty">Nothing filed here yet.</p>
{% endfor %}
</div>
</div>
{% endfor %}
{# Not a category. It has no row on the server, cannot be renamed or
deleted, and is not in the reorder - it is simply where the rest
are. Drawn only when it holds something, because a permanently
empty heading is worse than no heading. #}
{% if nav_tree.ungrouped %}
<div class="navcat navcat-fixed" data-cat="">
<div class="navcat-h">
<span class="navcat-tw"></span>
<span class="navcat-nm loose">Ungrouped</span>
<span class="navcat-n">{{ nav_tree.ungrouped|length }}</span>
</div>
<div class="navcat-b">
{% for item in nav_tree.ungrouped %}
{% include "_navitem.html" %}
{% endfor %}
</div>
</div>
{% endif %}
{% if not nav_tree.categories and not nav_tree.ungrouped %}
<p class="navcat-empty">
No categories yet.{% if nav_tree.can_arrange %} Use + to add one.{% endif %}
</p>
{% endif %}
</div>
</div>
</nav>
</aside>
<main class="main">
{% block content %}{% endblock %}
</main>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/shell.js"></script>
{% block page_scripts %}{% endblock %}
{% endblock %}