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:
thalladaandClaude Fable 5.1 committed 2026-09-03 21:50:05 +00:00
1 parent 29c66da865
commit ca910d99e5
15 files changed
+610 -19

No files matched your search

+3
View File
@@ -16,6 +16,7 @@ pub struct PublicIssue {
pub date: Date,
pub issue_number: i64,
pub display_date: String,
pub short_date: String,
pub article_count: i64,
pub reading_minutes: i64,
pub stats_line: String,
@@ -129,6 +130,7 @@ impl From<&Issue> for PublicIssue {
date: issue.meta.date,
issue_number: issue.meta.issue_number,
display_date: issue.meta.display_date.clone(),
short_date: crate::pipeline::short_display_date(issue.meta.date),
article_count: issue.meta.article_count,
reading_minutes: issue.meta.reading_minutes,
stats_line: issue.meta.stats_line(),
@@ -359,6 +361,7 @@ fn empty_issue() -> PublicIssue {
date: "1970-01-01".parse().expect("valid epoch date"),
issue_number: 0,
display_date: String::new(),
short_date: String::new(),
article_count: 0,
reading_minutes: 0,
stats_line: String::new(),