Web dashboard v2 step 4: table-of-contents sidebar

Signed-in issue pages (issue, article, world, behind) get a chapter list
that marks where the reader is. One `issue_toc` helper builds it for all
four handlers: the picks in issue order numbered across sections, then
World Briefing, Behind the paper and a colophon anchor, with
position/total over the navigable chapters.

At `lg` and up it is a sticky left column with its own sticky header
(issue number, "Chapter N of M", a 2px progress bar); below `lg` the same
`<nav>` collapses behind a sticky hamburger bar and drops down as a
panel that closes on link tap, Escape and outside tap. Without JS the
panel is simply visible; `theme.js` marks the document scripted before
paint so it never flashes open. Article pages advance the progress bar
with scroll position through `requestAnimationFrame`, and the bar is a
`<progress>` element because a percentage width would need an inline
style the CSP forbids.

Also fixes the phone "ears" row from the step 2 review: the row no
longer wraps at 390px, the dateline has a short form below `sm:`, and
the toggle and account link stay on one line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NHyYupFdBiR4VfoUM7NjSM
This commit is contained in:
2026-09-03 21:50:05 +00:00
co-authored by Claude Fable 5.1
parent 29c66da865
commit ca910d99e5
15 changed files with 610 additions and 19 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "layout.html" %}{% block ears %}{% if empty %}<span>Morning edition</span>{% else %}<span>{{ issue.display_date }} <span class="hidden sm:inline">· No. {{ issue.issue_number }}</span></span>{% endif %}{% endblock %}{% block content %}
{% extends "layout.html" %}{% block ears %}{% if empty %}<span>Morning edition</span>{% else %}<span class="sm:hidden">{{ issue.short_date }}</span><span class="hidden sm:inline">{{ issue.display_date }} · No. {{ issue.issue_number }}</span>{% endif %}{% endblock %}{% block content %}
<article class="mx-auto mt-8 max-w-[68ch] px-4 sm:mt-12 sm:px-6">
{% if empty %}<div class="py-16 text-center"><p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">Morning edition</p><h1 class="mt-3 text-4xl font-semibold leading-[1.1] tracking-[-0.01em]">No issue yet</h1><p class="mt-4 text-ink-2">The first issue has not been published.</p></div>{% else %}
<header class="mb-10 text-center"><p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">{{ issue.display_date }} · No. {{ issue.issue_number }}</p><p class="mt-2 font-sans text-sm text-muted">{{ issue.stats_line }}</p><p class="mx-auto mt-5 max-w-[58ch] text-lg italic leading-relaxed text-ink-2">A personal morning paper, assembled daily; the selection is the reader's, the words are the authors'.</p></header>