mobile fixes

This commit is contained in:
Levi Neuwirth 2026-04-12 15:29:35 -04:00
parent 41bbbd799b
commit c3fa26f60e
5 changed files with 201 additions and 13 deletions

View File

@ -291,10 +291,7 @@ nav.site-nav {
@media (max-width: 540px) { @media (max-width: 540px) {
.nav-logo { .nav-logo {
position: static; display: none;
height: 2rem;
width: 2rem;
flex-shrink: 0;
} }
.nav-row-primary { .nav-row-primary {
@ -345,6 +342,59 @@ nav.site-nav {
.nav-portals a { .nav-portals a {
padding: 0.3rem 0.55rem; padding: 0.3rem 0.55rem;
} }
/* Extra bottom padding so the last content line isn't hidden
behind the fixed mobile TOC bar. */
body {
padding-bottom: 2.5rem;
}
/* Mobile TOC bar fixed bottom strip: progress line + current
section label. toc.js sets --toc-progress and syncs the label. */
#toc-mobile-bar {
display: flex;
align-items: center;
position: fixed;
bottom: 0; left: 0; right: 0;
height: 2.25rem;
background: var(--bg-nav);
border-top: 1px solid var(--border);
padding: 0 1rem;
z-index: 90;
overflow: hidden;
}
/* Progress track (faint full-width line at top of bar) */
#toc-mobile-bar::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: var(--border);
}
/* Progress fill — grows with scroll via --toc-progress */
#toc-mobile-bar::after {
content: '';
position: absolute;
top: 0; left: 0;
height: 2px;
width: calc(var(--toc-progress, 0) * 100%);
background: var(--text-muted);
transition: width 0.12s ease;
}
.toc-mobile-label {
font-family: var(--font-sans);
font-size: 0.68rem;
font-weight: 600;
font-variant-caps: all-small-caps;
letter-spacing: 0.07em;
color: var(--text-faint);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
} }
/* Row 2: portal links — hidden until nav.js adds .is-open */ /* Row 2: portal links — hidden until nav.js adds .is-open */
@ -386,6 +436,12 @@ nav.site-nav {
and the --toc-progress custom property for the progress bar. and the --toc-progress custom property for the progress bar.
============================================================ */ ============================================================ */
/* Mobile TOC bar hidden on all viewports by default; the 540px
media query below makes it visible on mobile only. */
#toc-mobile-bar {
display: none;
}
#toc { #toc {
font-family: var(--font-sans); font-family: var(--font-sans);
font-size: 0.85rem; font-size: 0.85rem;

View File

@ -155,3 +155,64 @@ a.footnote-ref {
a.footnote-ref:hover { a.footnote-ref:hover {
color: var(--text-muted); color: var(--text-muted);
} }
/* ============================================================
MOBILE SIDENOTE POPUP
Bottom sheet shown when tapping a sidenote ref on narrow
viewports (where .sidenote is hidden). The overlay is
display:none by default; sidenotes.js adds .is-open to show it.
No media query needed JS only opens the popup when the
sidenote span is hidden, so it never fires on wide viewports.
============================================================ */
.sidenote-popup-overlay {
display: none;
position: fixed;
inset: 0;
z-index: 500;
background: rgba(0, 0, 0, 0.45);
align-items: flex-end;
}
.sidenote-popup-overlay.is-open {
display: flex;
}
.sidenote-popup {
background: var(--bg);
border-top: 1px solid var(--border);
border-radius: 0.75rem 0.75rem 0 0;
padding: 1.25rem 1.25rem 2.75rem;
width: 100%;
max-height: 65vh;
overflow-y: auto;
position: relative;
font-family: var(--font-serif);
font-size: 0.9rem;
line-height: 1.6;
color: var(--text);
outline: none;
}
.sidenote-popup-close {
position: absolute;
top: 0.7rem;
right: 0.9rem;
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
color: var(--text-faint);
cursor: pointer;
padding: 0.1rem 0.35rem;
}
.sidenote-popup-close:hover {
color: var(--text);
}
/* Preserve sidenote-num badge style inside the popup */
.sidenote-popup .sidenote-num {
display: inline-block;
}

View File

@ -97,12 +97,16 @@
sn.addEventListener('mouseenter', function () { hovering = true; update(); }); sn.addEventListener('mouseenter', function () { hovering = true; update(); });
sn.addEventListener('mouseleave', function () { hovering = false; update(); }); sn.addEventListener('mouseleave', function () { hovering = false; update(); });
/* Click on the superscript link: sticky focus on wide viewports, /* Click on the superscript link: sticky focus on wide viewports;
normal anchor scroll on narrow viewports (sidenote hidden). */ mobile popup on narrow viewports (sidenote hidden). */
const link = ref.querySelector('a'); const link = ref.querySelector('a');
if (link) { if (link) {
link.addEventListener('click', function (e) { link.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
if (getComputedStyle(sn).display === 'none') {
openMobilePopup(sn);
return;
}
toggleFocus(); toggleFocus();
}); });
@ -126,6 +130,65 @@
} }
} }
/* ------------------------------------------------------------------ */
/* Mobile popup — bottom sheet for narrow viewports where .sidenote */
/* is display:none. Created lazily on first use. */
/* ------------------------------------------------------------------ */
var mobileOverlay = null;
function ensureMobileOverlay() {
if (mobileOverlay) return;
var overlay = document.createElement('div');
overlay.className = 'sidenote-popup-overlay';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-label', 'Note');
var sheet = document.createElement('div');
sheet.className = 'sidenote-popup';
sheet.setAttribute('tabindex', '-1');
var closeBtn = document.createElement('button');
closeBtn.className = 'sidenote-popup-close';
closeBtn.setAttribute('aria-label', 'Close note');
closeBtn.textContent = '\u00d7'; /* × */
var body = document.createElement('div');
body.className = 'sidenote-popup-body';
sheet.appendChild(closeBtn);
sheet.appendChild(body);
overlay.appendChild(sheet);
document.body.appendChild(overlay);
overlay.addEventListener('click', function (e) {
if (!sheet.contains(e.target)) closeMobilePopup();
});
closeBtn.addEventListener('click', closeMobilePopup);
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && mobileOverlay &&
mobileOverlay.classList.contains('is-open')) {
closeMobilePopup();
}
});
mobileOverlay = overlay;
}
function openMobilePopup(sn) {
ensureMobileOverlay();
mobileOverlay.querySelector('.sidenote-popup-body').innerHTML = sn.innerHTML;
mobileOverlay.classList.add('is-open');
mobileOverlay.querySelector('.sidenote-popup').focus();
}
function closeMobilePopup() {
if (mobileOverlay) mobileOverlay.classList.remove('is-open');
}
/* Click anywhere outside a focused pair dismisses it. */ /* Click anywhere outside a focused pair dismisses it. */
document.addEventListener('click', function (e) { document.addEventListener('click', function (e) {
if (focusedPair && if (focusedPair &&

View File

@ -22,6 +22,8 @@
const label = toc.querySelector('.toc-active-label'); const label = toc.querySelector('.toc-active-label');
const toggleBtn = toc.querySelector('.toc-toggle'); const toggleBtn = toc.querySelector('.toc-toggle');
const mobileBar = document.getElementById('toc-mobile-bar');
const mobileLabel = mobileBar && mobileBar.querySelector('.toc-mobile-label');
const pageTitleEl = document.querySelector('#markdownBody .page-title'); const pageTitleEl = document.querySelector('#markdownBody .page-title');
const pageTitle = pageTitleEl ? pageTitleEl.textContent.trim() : 'Contents'; const pageTitle = pageTitleEl ? pageTitleEl.textContent.trim() : 'Contents';
@ -29,14 +31,15 @@
function activateTitle() { function activateTitle() {
links.forEach(a => a.classList.remove('is-active')); links.forEach(a => a.classList.remove('is-active'));
if (label) label.textContent = pageTitle; if (label) label.textContent = pageTitle;
if (mobileLabel) mobileLabel.textContent = pageTitle;
} }
function activate(id) { function activate(id) {
links.forEach(a => a.classList.toggle('is-active', a.dataset.target === id)); links.forEach(a => a.classList.toggle('is-active', a.dataset.target === id));
if (label) {
const activeLink = linkMap.get(id); const activeLink = linkMap.get(id);
label.textContent = activeLink ? activeLink.textContent : pageTitle; const text = activeLink ? activeLink.textContent : pageTitle;
} if (label) label.textContent = text;
if (mobileLabel) mobileLabel.textContent = text;
} }
// Collapse / expand. The collapsed state is hidden from // Collapse / expand. The collapsed state is hidden from
@ -70,11 +73,13 @@
} }
// Progress indicator — drives the horizontal bar under .toc-header. // Progress indicator — drives the horizontal bar under .toc-header
// and the mobile bar's progress line.
function updateProgress() { function updateProgress() {
const scrollable = document.documentElement.scrollHeight - window.innerHeight; const scrollable = document.documentElement.scrollHeight - window.innerHeight;
const progress = scrollable > 0 ? Math.min(1, window.scrollY / scrollable) : 0; const progress = scrollable > 0 ? Math.min(1, window.scrollY / scrollable) : 0;
toc.style.setProperty('--toc-progress', progress); toc.style.setProperty('--toc-progress', progress);
if (mobileBar) mobileBar.style.setProperty('--toc-progress', progress);
} }
window.addEventListener('scroll', updateProgress, { passive: true }); window.addEventListener('scroll', updateProgress, { passive: true });
updateProgress(); updateProgress();

View File

@ -14,4 +14,7 @@
$body$ $body$
</main> </main>
</div> </div>
<div id="toc-mobile-bar" aria-hidden="true">
<span class="toc-mobile-label">Contents</span>
</div>
$partial("templates/partials/page-footer.html")$ $partial("templates/partials/page-footer.html")$