Commit 81fc1d9 h
Changes: 2 files +109 -13
Changes (Side-by-Side)
share/jenny/layout/page.sh +68 -9
URL_PREFIX=${URL_PREFIX:-.} | |||
| 39 | # touching this function. |
39 | # touching this function. |
| 40 | page_nav() { |
40 | page_nav() { |
| 41 | [ -n "${PAGE_NAV:-}" ] || return 0 |
41 | [ -n "${PAGE_NAV:-}" ] || return 0 |
| 42 | # <details> rather than a scripted toggle: the open/closed state, the |
||
| 43 | # keyboard handling and the screen-reader semantics are the element's own, |
||
| 44 | # and a nav that needs JavaScript to open is a nav that does not open when |
||
| 45 | # the script fails. Wide screens override it back to always-open, where a |
||
| 46 | # summary would be noise beside a sidebar that already fits. |
||
| 47 | printf '<details class="page-nav-wrap" open>\n' |
||
| 48 | printf ' <summary class="page-nav-toggle">Contents</summary>\n' |
||
| 42 | printf '<nav class="page-nav">\n' |
49 | printf '<nav class="page-nav">\n' |
| 43 | printf '%s\n' "$PAGE_NAV" | while IFS="$(printf '\t')" read -r kind depth label path; do |
50 | printf '%s\n' "$PAGE_NAV" | while IFS="$(printf '\t')" read -r kind depth label path; do |
| 44 | [ -n "$label" ] || continue |
51 | [ -n "$label" ] || continue |
page_nav() { | |||
| 59 | esac |
66 | esac |
| 60 | done |
67 | done |
| 61 | printf '</nav>\n' |
68 | printf '</nav>\n' |
| 69 | printf '</details>\n' |
||
| 62 | } |
70 | } |
| 63 | |
71 | |
| 64 | # diagram_script |
72 | # diagram_script |
cat << _EOF_ | |||
| 284 | 74ch and the body came out 537px against a post's 540 -- a |
292 | 74ch and the body came out 537px against a post's 540 -- a |
| 285 | difference with no cause the reader could see. Widening here rather |
293 | difference with no cause the reader could see. Widening here rather |
| 286 | than narrowing the body keeps the measure identical everywhere. */ |
294 | than narrowing the body keeps the measure identical everywhere. */ |
| 287 | article:has(.page-nav) { max-width: calc(74ch + var(--page-nav-width) + var(--page-nav-gap)); } |
||
| 295 | article:has(.page-nav-wrap) { max-width: calc(74ch + var(--page-nav-width) + var(--page-nav-gap)); } |
||
| 288 | /* A bare URL has no spaces to wrap at, so on a narrow screen one |
296 | /* A bare URL has no spaces to wrap at, so on a narrow screen one |
| 289 | long link sets the width of the whole page -- a reference to |
297 | long link sets the width of the whole page -- a reference to |
| 290 | https://github.com/hmngwy/jenny made this post 375px wide at a |
298 | https://github.com/hmngwy/jenny made this post 375px wide at a |
cat << _EOF_ | |||
| 429 | width is only a suggestion, so the nav column shrank to whatever the |
437 | width is only a suggestion, so the nav column shrank to whatever the |
| 430 | longest label happened to be and the body's left edge moved with it. |
438 | longest label happened to be and the body's left edge moved with it. |
| 431 | The floor keeps the column a fixed 6rem regardless of the labels. */ |
439 | The floor keeps the column a fixed 6rem regardless of the labels. */ |
| 432 | .page-nav { display: table-cell; vertical-align: top; |
||
| 433 | width: var(--page-nav-width); |
||
| 434 | min-width: var(--page-nav-width); |
||
| 435 | max-width: var(--page-nav-width); |
||
| 436 | margin: 0; padding: 0; font-size: .9em; } |
||
| 440 | /* The wrapper is the sidebar cell; the nav inside it is a plain |
||
| 441 | block. The <details> only does anything on a phone -- here the |
||
| 442 | summary is hidden and the element is forced open, so a wide screen |
||
| 443 | renders exactly the sidebar it did before. */ |
||
| 444 | .page-nav-wrap { display: table-cell; vertical-align: top; |
||
| 445 | width: var(--page-nav-width); |
||
| 446 | min-width: var(--page-nav-width); |
||
| 447 | max-width: var(--page-nav-width); |
||
| 448 | margin: 0; padding: 0; font-size: .9em; } |
||
| 449 | .page-nav-toggle { display: none; } |
||
| 450 | /* The element ships open, so a sidebar renders on a wide screen and, |
||
| 451 | if the script below never runs, on a narrow one too -- failing to an |
||
| 452 | expanded nav rather than a hidden one. Overriding a closed |
||
| 453 | <details> from CSS was tried instead and does not work: a closed |
||
| 454 | details suppresses painting of its contents whatever display says, |
||
| 455 | so the column measured correct while rendering blank. */ |
||
| 456 | .page-nav { margin: 0; padding: 0; } |
||
| 437 | /* A label with no space in it -- a long slug, a URL -- cannot wrap, so |
457 | /* A label with no space in it -- a long slug, a URL -- cannot wrap, so |
| 438 | the column grew to fit it and pushed the body right; min-width is a |
458 | the column grew to fit it and pushed the body right; min-width is a |
| 439 | floor, not a ceiling. The ceiling plus overflow-wrap keeps the |
459 | floor, not a ceiling. The ceiling plus overflow-wrap keeps the |
cat << _EOF_ | |||
| 489 | /* With no nav there is no sidebar to sit beside, so the body starts at |
509 | /* With no nav there is no sidebar to sit beside, so the body starts at |
| 490 | the left margin -- the same place a post's body starts now that the |
510 | the left margin -- the same place a post's body starts now that the |
| 491 | way back is below the text rather than beside it. */ |
511 | way back is below the text rather than beside it. */ |
| 492 | .page-wrapper:not(:has(.page-nav)) .contents-cell { display: block; |
||
| 512 | .page-wrapper:not(:has(.page-nav-wrap)) .contents-cell { display: block; |
||
| 493 | padding-left: 0; } |
513 | padding-left: 0; } |
| 494 | /* Identical to the post layout's. A block, not an inline-block: the |
514 | /* Identical to the post layout's. A block, not an inline-block: the |
| 495 | body no longer sits beside anything -- the back arrow that used to |
515 | body no longer sits beside anything -- the back arrow that used to |
cat << _EOF_ | |||
| 504 | an anonymous table wrapper and stays in its column. */ |
524 | an anonymous table wrapper and stays in its column. */ |
| 505 | @media (max-width: 735px) { |
525 | @media (max-width: 735px) { |
| 506 | .page-wrapper, |
526 | .page-wrapper, |
| 507 | .page-nav, |
||
| 527 | .page-nav-wrap, |
||
| 508 | .contents-cell { display: block; } |
528 | .contents-cell { display: block; } |
| 509 | .page-nav { width: auto; min-width: 0; margin-bottom: 2em; } |
||
| 529 | /* max-width was the reason labels wrapped at a 11px indent on a |
||
| 530 | phone: the narrow block reset width and min-width but left the |
||
| 531 | 6rem ceiling in place, so the nav stayed 96px wide while the rest |
||
| 532 | of the page went full width. All three go together. */ |
||
| 533 | .page-nav-wrap { width: auto; min-width: 0; max-width: none; |
||
| 534 | margin-bottom: 2em; font-size: 1em; } |
||
| 535 | /* Collapsed by default here, and only here. A nav ahead of the text |
||
| 536 | costs a quarter of the screen before a reader reaches the title; |
||
| 537 | behind a summary it costs one line and is still one tap away. */ |
||
| 538 | |
||
| 539 | .page-nav-toggle { display: block; cursor: pointer; |
||
| 540 | text-transform: uppercase; font-size: .9em; |
||
| 541 | color: var(--text-muted); padding: .25em 0; |
||
| 542 | border-bottom: 1px solid var(--border-lighter); } |
||
| 543 | .page-nav-toggle::marker { color: var(--text-muted); } |
||
| 544 | .page-nav { margin-top: .75em; } |
||
| 510 | /* The sidebar's gap goes with the sidebar: stacked above the body, |
545 | /* The sidebar's gap goes with the sidebar: stacked above the body, |
| 511 | it is not a column to clear. Left in place it indented the body on |
546 | it is not a column to clear. Left in place it indented the body on |
| 512 | a phone and forced horizontal scroll. */ |
547 | a phone and forced horizontal scroll. */ |
cat << _EOF_ | |||
| 609 | } else if (mediaQuery.addListener) { |
644 | } else if (mediaQuery.addListener) { |
| 610 | mediaQuery.addListener(handleSystemChange); |
645 | mediaQuery.addListener(handleSystemChange); |
| 611 | } |
646 | } |
| 647 | // The page nav ships open so it renders as a sidebar on a wide |
||
| 648 | // screen, and so that a failure here leaves an expanded nav rather |
||
| 649 | // than one the reader cannot open. On a narrow screen it starts |
||
| 650 | // collapsed: 264px of links ahead of the title is a quarter of a |
||
| 651 | // phone screen spent before the text begins. |
||
| 652 | // |
||
| 653 | // Closed only on the first paint at a narrow width. Re-collapsing on |
||
| 654 | // every resize would shut the nav under a reader who had just opened |
||
| 655 | // it, and matching the CSS breakpoint here keeps the two from |
||
| 656 | // disagreeing about what "narrow" means. |
||
| 657 | // Deferred: this script is in <head>, so the nav does not exist yet |
||
| 658 | // when it first runs -- querying for it here found nothing and the |
||
| 659 | // nav stayed open on a phone. |
||
| 660 | var collapseNav = function() { |
||
| 661 | var navWrap = document.querySelector('.page-nav-wrap'); |
||
| 662 | if (navWrap && window.matchMedia('(max-width: 735px)').matches) { |
||
| 663 | navWrap.removeAttribute('open'); |
||
| 664 | } |
||
| 665 | }; |
||
| 666 | if (document.readyState === 'loading') { |
||
| 667 | document.addEventListener('DOMContentLoaded', collapseNav); |
||
| 668 | } else { |
||
| 669 | collapseNav(); |
||
| 670 | } |
||
| 612 | document.addEventListener('click', function(e) { |
671 | document.addEventListener('click', function(e) { |
| 613 | var toggle = e.target.closest('.theme-toggle'); |
672 | var toggle = e.target.closest('.theme-toggle'); |
| 614 | if (!toggle) return; |
673 | if (!toggle) return; |
test/regressions.sh +41 -4
assert_file_contains "$nav_sb/.dist/test.html" '.page-wrapper { display: table' | |||
| 1743 | assert_file_contains "$nav_sb/.dist/test.html" '.contents-cell { display: table-cell' |
1743 | assert_file_contains "$nav_sb/.dist/test.html" '.contents-cell { display: table-cell' |
| 1744 | |
1744 | |
| 1745 | it "the sidebar is a table cell aligned to the top of its row" |
1745 | it "the sidebar is a table cell aligned to the top of its row" |
| 1746 | nav_rule=$("$SED" -n '/^ \.page-nav { display: table-cell;/,/}/p' "$nav_sb/.dist/test.html") |
||
| 1746 | # The cell is the <details> wrapper now: the nav is collapsible on a phone, so |
||
| 1747 | # the sidebar column belongs to the element that holds the summary as well as |
||
| 1748 | # the list. |
||
| 1749 | nav_rule=$("$SED" -n '/^ \.page-nav-wrap { display: table-cell;/,/}/p' "$nav_sb/.dist/test.html") |
||
| 1747 | assert_contains "$nav_rule" 'vertical-align: top' |
1750 | assert_contains "$nav_rule" 'vertical-align: top' |
| 1748 | |
1751 | |
| 1749 | it "the sidebar column does not depend on its labels" |
1752 | it "the sidebar column does not depend on its labels" |
it "the sidebar carries no positioning offset" | |||
| 1758 | assert_file_not_contains "$nav_sb/.dist/test.html" '.page-nav { position: absolute' |
1761 | assert_file_not_contains "$nav_sb/.dist/test.html" '.page-nav { position: absolute' |
| 1759 | |
1762 | |
| 1760 | it "a page with no nav takes the body indent, not the sidebar column" |
1763 | it "a page with no nav takes the body indent, not the sidebar column" |
| 1761 | assert_file_contains "$nav_sb/.dist/plain.html" '.page-wrapper:not(:has(.page-nav))' |
||
| 1764 | assert_file_contains "$nav_sb/.dist/plain.html" '.page-wrapper:not(:has(.page-nav-wrap))' |
||
| 1762 | |
1765 | |
| 1763 | it "the body starts at the left margin, with no column reserved" |
1766 | it "the body starts at the left margin, with no column reserved" |
| 1764 | # The indent existed only to make room for the back arrow beside the body. |
1767 | # The indent existed only to make room for the back arrow beside the body. |
assert_file_contains "$nav_sb/.dist/plain.html" '.home { display: block; margin- | |||
| 1771 | it "a sidebar widens the article rather than narrowing the body" |
1774 | it "a sidebar widens the article rather than narrowing the body" |
| 1772 | # 74ch is sized for a post: arrow column plus a 60ch body. Letting the sidebar |
1775 | # 74ch is sized for a post: arrow column plus a 60ch body. Letting the sidebar |
| 1773 | # eat into it made the body 537px against a post's 540. |
1776 | # eat into it made the body 537px against a post's 540. |
| 1774 | assert_file_contains "$nav_sb/.dist/test.html" 'article:has(.page-nav) { max-width: calc(74ch' |
||
| 1777 | assert_file_contains "$nav_sb/.dist/test.html" 'article:has(.page-nav-wrap) { max-width: calc(74ch' |
||
| 1775 | |
1778 | |
| 1776 | it "the body column is declared identically to a post's" |
1779 | it "the body column is declared identically to a post's" |
| 1777 | # The user's constraint: the body of a page should differ from the body of a |
1780 | # The user's constraint: the body of a page should differ from the body of a |
it "narrow screens collapse the whole table, not just one cell" | |||
| 1834 | # stays in its column, so all three have to leave table layout together. |
1837 | # stays in its column, so all three have to leave table layout together. |
| 1835 | narrow=$("$SED" -n '/@media (max-width: 735px)/,/^ }/p' "$nav_sb/.dist/test.html") |
1838 | narrow=$("$SED" -n '/@media (max-width: 735px)/,/^ }/p' "$nav_sb/.dist/test.html") |
| 1836 | assert_contains "$narrow" '.page-wrapper,' |
1839 | assert_contains "$narrow" '.page-wrapper,' |
| 1837 | assert_contains "$narrow" '.page-nav,' |
||
| 1840 | assert_contains "$narrow" '.page-nav-wrap,' |
||
| 1838 | assert_contains "$narrow" '.contents-cell { display: block' |
1841 | assert_contains "$narrow" '.contents-cell { display: block' |
| 1839 | |
1842 | |
| 1840 | it "the narrow sidebar releases its fixed column" |
1843 | it "the narrow sidebar releases its fixed column" |
| 1844 | # All three of width, min-width and max-width. The ceiling was the one left |
||
| 1845 | # behind before: the nav stayed 96px wide on a phone while the rest of the page |
||
| 1846 | # went full width, so every label wrapped at an 11px indent. |
||
| 1841 | assert_contains "$narrow" 'min-width: 0' |
1847 | assert_contains "$narrow" 'min-width: 0' |
| 1848 | assert_contains "$narrow" 'max-width: none' |
||
| 1849 | |
||
| 1850 | it "the nav is a details element, so the toggle needs no script to work" |
||
| 1851 | # <details> brings its own open/closed state, keyboard handling and screen |
||
| 1852 | # reader semantics. A nav that needs JavaScript to open is a nav that does not |
||
| 1853 | # open when the script fails. |
||
| 1854 | assert_file_contains "$nav_sb/.dist/test.html" '<details class="page-nav-wrap" open>' |
||
| 1855 | assert_file_contains "$nav_sb/.dist/test.html" '<summary class="page-nav-toggle">' |
||
| 1856 | |
||
| 1857 | it "it ships open and is closed on a phone by script" |
||
| 1858 | # Shipping open means a wide screen renders the sidebar it always did, and a |
||
| 1859 | # script failure leaves an expanded nav rather than one the reader cannot |
||
| 1860 | # reach. Overriding a closed <details> from CSS was tried first and does not |
||
| 1861 | # work -- a closed details suppresses painting of its contents whatever |
||
| 1862 | # display says, so the sidebar measured correct and rendered blank. |
||
| 1863 | assert_file_contains "$nav_sb/.dist/test.html" "matchMedia('(max-width: 735px)').matches" |
||
| 1864 | assert_file_contains "$nav_sb/.dist/test.html" "navWrap.removeAttribute('open')" |
||
| 1865 | |
||
| 1866 | it "the collapse waits for the nav to exist" |
||
| 1867 | # The script is in <head>, so it first ran before the nav was parsed: the |
||
| 1868 | # query found nothing and the nav stayed open on a phone. |
||
| 1869 | assert_file_contains "$nav_sb/.dist/test.html" "document.addEventListener('DOMContentLoaded', collapseNav)" |
||
| 1870 | |
||
| 1871 | it "the summary is hidden on a wide screen" |
||
| 1872 | # The toggle is a phone affordance; beside a sidebar that already fits it is |
||
| 1873 | # noise. |
||
| 1874 | assert_file_contains "$nav_sb/.dist/test.html" '.page-nav-toggle { display: none; }' |
||
| 1875 | |
||
| 1876 | it "a page with no nav gets no toggle" |
||
| 1877 | assert_file_not_contains "$nav_sb/.dist/plain.html" '<details class="page-nav-wrap"' |
||
| 1878 | assert_file_not_contains "$nav_sb/.dist/plain.html" '<summary' |
||
| 1842 | |
1879 | |
| 1843 | it "the build is clean for nav sidebar" |
1880 | it "the build is clean for nav sidebar" |
| 1844 | assert_eq "$stderr_output" "" |
1881 | assert_eq "$stderr_output" "" |
Raw
commit 81fc1d98b9b6e67362cb646ba963dbdf8c56e334
Author: Pat Ambrosio <cp.ambrosio@gmail.com>
Date: Wed Aug 26 12:28:07 2026 +0800
Collapse the page nav behind a toggle on a phone
264px of links stood between the header and the title -- 28% of a 440px
screen spent before the text begins. And every label wrapped: "Why this
shape" broke across two lines at an 11px indent, which reads as two
entries rather than one.
The wrapping was a missed reset. The narrow block cleared the nav's width
and min-width but left max-width at 6rem, so the column stayed 96px wide
while the rest of the page went full width. All three go together now, and
the labels fit on one line each.
The nav is a <details> with a summary. The element brings its own open
state, keyboard handling and screen reader semantics; a nav that needs
script to open is a nav that does not open when the script fails. It ships
open, so a wide screen renders the sidebar it always did and a failure
leaves an expanded nav rather than a hidden one, and a line of script
closes it on the first paint below 735px -- the same width the sidebar
already stacks at.
Two approaches were tried and rejected on evidence. Shipping it closed and
forcing it open with CSS renders blank on desktop: a closed <details>
suppresses painting of its contents whatever display says, so the column
measured correct -- 96px, 7 items, first item 24px at the right offset --
while showing nothing. And the collapse ran too early: the script is in
<head>, so the query found no nav and the phone stayed expanded. It waits
for DOMContentLoaded now.
Collapsed, the nav is one line and the title moves from y=356 to y=128.
Opened, it takes the full article width with no label wrapping. Desktop is
untouched: summary hidden, sidebar visible, tops aligned at 64, body under
title at 152.
The sidebar cell is now the wrapper rather than the nav, so the :has()
selectors and the table-cell rules move with it. Three existing tests
caught the rename.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnSAstgnDYdKPwLGv1YZE5
diff --git a/share/jenny/layout/page.sh b/share/jenny/layout/page.sh
index 4e96dd0..ab258f6 100755
--- a/share/jenny/layout/page.sh
+++ b/share/jenny/layout/page.sh
@@ -39,6 +39,13 @@ URL_PREFIX=${URL_PREFIX:-.}
# touching this function.
page_nav() {
[ -n "${PAGE_NAV:-}" ] || return 0
+ # <details> rather than a scripted toggle: the open/closed state, the
+ # keyboard handling and the screen-reader semantics are the element's own,
+ # and a nav that needs JavaScript to open is a nav that does not open when
+ # the script fails. Wide screens override it back to always-open, where a
+ # summary would be noise beside a sidebar that already fits.
+ printf '<details class="page-nav-wrap" open>\n'
+ printf ' <summary class="page-nav-toggle">Contents</summary>\n'
printf '<nav class="page-nav">\n'
printf '%s\n' "$PAGE_NAV" | while IFS="$(printf '\t')" read -r kind depth label path; do
[ -n "$label" ] || continue
@@ -59,6 +66,7 @@ page_nav() {
esac
done
printf '</nav>\n'
+ printf '</details>\n'
}
# diagram_script
@@ -284,7 +292,7 @@ cat << _EOF_
74ch and the body came out 537px against a post's 540 -- a
difference with no cause the reader could see. Widening here rather
than narrowing the body keeps the measure identical everywhere. */
- article:has(.page-nav) { max-width: calc(74ch + var(--page-nav-width) + var(--page-nav-gap)); }
+ article:has(.page-nav-wrap) { max-width: calc(74ch + var(--page-nav-width) + var(--page-nav-gap)); }
/* A bare URL has no spaces to wrap at, so on a narrow screen one
long link sets the width of the whole page -- a reference to
https://github.com/hmngwy/jenny made this post 375px wide at a
@@ -429,11 +437,23 @@ cat << _EOF_
width is only a suggestion, so the nav column shrank to whatever the
longest label happened to be and the body's left edge moved with it.
The floor keeps the column a fixed 6rem regardless of the labels. */
- .page-nav { display: table-cell; vertical-align: top;
- width: var(--page-nav-width);
- min-width: var(--page-nav-width);
- max-width: var(--page-nav-width);
- margin: 0; padding: 0; font-size: .9em; }
+ /* The wrapper is the sidebar cell; the nav inside it is a plain
+ block. The <details> only does anything on a phone -- here the
+ summary is hidden and the element is forced open, so a wide screen
+ renders exactly the sidebar it did before. */
+ .page-nav-wrap { display: table-cell; vertical-align: top;
+ width: var(--page-nav-width);
+ min-width: var(--page-nav-width);
+ max-width: var(--page-nav-width);
+ margin: 0; padding: 0; font-size: .9em; }
+ .page-nav-toggle { display: none; }
+ /* The element ships open, so a sidebar renders on a wide screen and,
+ if the script below never runs, on a narrow one too -- failing to an
+ expanded nav rather than a hidden one. Overriding a closed
+ <details> from CSS was tried instead and does not work: a closed
+ details suppresses painting of its contents whatever display says,
+ so the column measured correct while rendering blank. */
+ .page-nav { margin: 0; padding: 0; }
/* A label with no space in it -- a long slug, a URL -- cannot wrap, so
the column grew to fit it and pushed the body right; min-width is a
floor, not a ceiling. The ceiling plus overflow-wrap keeps the
@@ -489,7 +509,7 @@ cat << _EOF_
/* With no nav there is no sidebar to sit beside, so the body starts at
the left margin -- the same place a post's body starts now that the
way back is below the text rather than beside it. */
- .page-wrapper:not(:has(.page-nav)) .contents-cell { display: block;
+ .page-wrapper:not(:has(.page-nav-wrap)) .contents-cell { display: block;
padding-left: 0; }
/* Identical to the post layout's. A block, not an inline-block: the
body no longer sits beside anything -- the back arrow that used to
@@ -504,9 +524,24 @@ cat << _EOF_
an anonymous table wrapper and stays in its column. */
@media (max-width: 735px) {
.page-wrapper,
- .page-nav,
+ .page-nav-wrap,
.contents-cell { display: block; }
- .page-nav { width: auto; min-width: 0; margin-bottom: 2em; }
+ /* max-width was the reason labels wrapped at a 11px indent on a
+ phone: the narrow block reset width and min-width but left the
+ 6rem ceiling in place, so the nav stayed 96px wide while the rest
+ of the page went full width. All three go together. */
+ .page-nav-wrap { width: auto; min-width: 0; max-width: none;
+ margin-bottom: 2em; font-size: 1em; }
+ /* Collapsed by default here, and only here. A nav ahead of the text
+ costs a quarter of the screen before a reader reaches the title;
+ behind a summary it costs one line and is still one tap away. */
+
+ .page-nav-toggle { display: block; cursor: pointer;
+ text-transform: uppercase; font-size: .9em;
+ color: var(--text-muted); padding: .25em 0;
+ border-bottom: 1px solid var(--border-lighter); }
+ .page-nav-toggle::marker { color: var(--text-muted); }
+ .page-nav { margin-top: .75em; }
/* The sidebar's gap goes with the sidebar: stacked above the body,
it is not a column to clear. Left in place it indented the body on
a phone and forced horizontal scroll. */
@@ -609,6 +644,30 @@ cat << _EOF_
} else if (mediaQuery.addListener) {
mediaQuery.addListener(handleSystemChange);
}
+ // The page nav ships open so it renders as a sidebar on a wide
+ // screen, and so that a failure here leaves an expanded nav rather
+ // than one the reader cannot open. On a narrow screen it starts
+ // collapsed: 264px of links ahead of the title is a quarter of a
+ // phone screen spent before the text begins.
+ //
+ // Closed only on the first paint at a narrow width. Re-collapsing on
+ // every resize would shut the nav under a reader who had just opened
+ // it, and matching the CSS breakpoint here keeps the two from
+ // disagreeing about what "narrow" means.
+ // Deferred: this script is in <head>, so the nav does not exist yet
+ // when it first runs -- querying for it here found nothing and the
+ // nav stayed open on a phone.
+ var collapseNav = function() {
+ var navWrap = document.querySelector('.page-nav-wrap');
+ if (navWrap && window.matchMedia('(max-width: 735px)').matches) {
+ navWrap.removeAttribute('open');
+ }
+ };
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', collapseNav);
+ } else {
+ collapseNav();
+ }
document.addEventListener('click', function(e) {
var toggle = e.target.closest('.theme-toggle');
if (!toggle) return;
diff --git a/test/regressions.sh b/test/regressions.sh
index c0aa821..863818c 100755
--- a/test/regressions.sh
+++ b/test/regressions.sh
@@ -1743,7 +1743,10 @@ assert_file_contains "$nav_sb/.dist/test.html" '.page-wrapper { display: table'
assert_file_contains "$nav_sb/.dist/test.html" '.contents-cell { display: table-cell'
it "the sidebar is a table cell aligned to the top of its row"
-nav_rule=$("$SED" -n '/^ \.page-nav { display: table-cell;/,/}/p' "$nav_sb/.dist/test.html")
+# The cell is the <details> wrapper now: the nav is collapsible on a phone, so
+# the sidebar column belongs to the element that holds the summary as well as
+# the list.
+nav_rule=$("$SED" -n '/^ \.page-nav-wrap { display: table-cell;/,/}/p' "$nav_sb/.dist/test.html")
assert_contains "$nav_rule" 'vertical-align: top'
it "the sidebar column does not depend on its labels"
@@ -1758,7 +1761,7 @@ it "the sidebar carries no positioning offset"
assert_file_not_contains "$nav_sb/.dist/test.html" '.page-nav { position: absolute'
it "a page with no nav takes the body indent, not the sidebar column"
-assert_file_contains "$nav_sb/.dist/plain.html" '.page-wrapper:not(:has(.page-nav))'
+assert_file_contains "$nav_sb/.dist/plain.html" '.page-wrapper:not(:has(.page-nav-wrap))'
it "the body starts at the left margin, with no column reserved"
# The indent existed only to make room for the back arrow beside the body.
@@ -1771,7 +1774,7 @@ assert_file_contains "$nav_sb/.dist/plain.html" '.home { display: block; margin-
it "a sidebar widens the article rather than narrowing the body"
# 74ch is sized for a post: arrow column plus a 60ch body. Letting the sidebar
# eat into it made the body 537px against a post's 540.
-assert_file_contains "$nav_sb/.dist/test.html" 'article:has(.page-nav) { max-width: calc(74ch'
+assert_file_contains "$nav_sb/.dist/test.html" 'article:has(.page-nav-wrap) { max-width: calc(74ch'
it "the body column is declared identically to a post's"
# The user's constraint: the body of a page should differ from the body of a
@@ -1834,11 +1837,45 @@ it "narrow screens collapse the whole table, not just one cell"
# stays in its column, so all three have to leave table layout together.
narrow=$("$SED" -n '/@media (max-width: 735px)/,/^ }/p' "$nav_sb/.dist/test.html")
assert_contains "$narrow" '.page-wrapper,'
-assert_contains "$narrow" '.page-nav,'
+assert_contains "$narrow" '.page-nav-wrap,'
assert_contains "$narrow" '.contents-cell { display: block'
it "the narrow sidebar releases its fixed column"
+# All three of width, min-width and max-width. The ceiling was the one left
+# behind before: the nav stayed 96px wide on a phone while the rest of the page
+# went full width, so every label wrapped at an 11px indent.
assert_contains "$narrow" 'min-width: 0'
+assert_contains "$narrow" 'max-width: none'
+
+it "the nav is a details element, so the toggle needs no script to work"
+# <details> brings its own open/closed state, keyboard handling and screen
+# reader semantics. A nav that needs JavaScript to open is a nav that does not
+# open when the script fails.
+assert_file_contains "$nav_sb/.dist/test.html" '<details class="page-nav-wrap" open>'
+assert_file_contains "$nav_sb/.dist/test.html" '<summary class="page-nav-toggle">'
+
+it "it ships open and is closed on a phone by script"
+# Shipping open means a wide screen renders the sidebar it always did, and a
+# script failure leaves an expanded nav rather than one the reader cannot
+# reach. Overriding a closed <details> from CSS was tried first and does not
+# work -- a closed details suppresses painting of its contents whatever
+# display says, so the sidebar measured correct and rendered blank.
+assert_file_contains "$nav_sb/.dist/test.html" "matchMedia('(max-width: 735px)').matches"
+assert_file_contains "$nav_sb/.dist/test.html" "navWrap.removeAttribute('open')"
+
+it "the collapse waits for the nav to exist"
+# The script is in <head>, so it first ran before the nav was parsed: the
+# query found nothing and the nav stayed open on a phone.
+assert_file_contains "$nav_sb/.dist/test.html" "document.addEventListener('DOMContentLoaded', collapseNav)"
+
+it "the summary is hidden on a wide screen"
+# The toggle is a phone affordance; beside a sidebar that already fits it is
+# noise.
+assert_file_contains "$nav_sb/.dist/test.html" '.page-nav-toggle { display: none; }'
+
+it "a page with no nav gets no toggle"
+assert_file_not_contains "$nav_sb/.dist/plain.html" '<details class="page-nav-wrap"'
+assert_file_not_contains "$nav_sb/.dist/plain.html" '<summary'
it "the build is clean for nav sidebar"
assert_eq "$stderr_output" ""