diff --git a/public/css/arf.css b/public/css/arf.css index b0f6f03..2c52415 100644 --- a/public/css/arf.css +++ b/public/css/arf.css @@ -70,6 +70,8 @@ body { color: var(--color-text-primary); font-size: var(--font-size-base); font-family: var(--font-family); + margin: 0; + overflow: hidden; } noscript p { @@ -94,7 +96,8 @@ noscript p { svg { display: block; width: 100%; - height: auto; + /* Fill remaining viewport height below header + search bar */ + height: calc(100vh - 120px); cursor: grab; } @@ -103,20 +106,50 @@ svg:active { } #header { + display: flex; + align-items: center; + justify-content: center; + position: relative; font-size: var(--font-size-xl); font-weight: bold; - top: 20px; text-align: center; color: var(--color-header); + padding: var(--space-2) var(--space-4); +} + +#header-actions { + position: absolute; + right: var(--space-4); + top: 50%; + transform: translateY(-50%); + display: flex; + gap: var(--space-2); + align-items: center; +} + +#header-actions button { + background: none; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + font-family: var(--font-family); + padding: var(--space-1) var(--space-2); + cursor: pointer; + white-space: nowrap; +} + +#header-actions button:hover { + color: var(--color-text-primary); + border-color: var(--color-text-secondary); } .legend { - position: absolute; - top: 0px; - right: 0; - width: 330px; - font-size: var(--font-size-sm); - color: var(--color-legend); + display: none; +} + +#notes-content { + display: none; } .legend a { @@ -260,24 +293,97 @@ a { text-align: center; } -/* Mobile theme toggle (hidden on desktop) */ -#mobile-theme-toggle { - display: none; -} - -/* Mobile notes button (hidden on desktop) */ +/* Mobile floating notes button (hidden on desktop; header button used instead) */ #mobile-notes-btn { display: none; } -/* Notes overlay (hidden on desktop) */ +/* Notes overlay */ #notes-overlay { display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 299; } -/* Notes panel (hidden on desktop) */ +#notes-overlay.visible { + display: block; +} + +/* Notes panel (desktop: side panel) */ #notes-panel { - display: none; + display: flex; + flex-direction: column; + position: fixed; + top: 0; + right: 0; + width: 400px; + max-width: 100vw; + height: 100vh; + background: var(--color-surface); + z-index: 300; + transform: translateX(100%); + transition: transform 250ms ease; + will-change: transform; + box-shadow: var(--shadow-md); +} + +#notes-panel.open { + transform: translateX(0); +} + +#notes-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-4); + border-bottom: 1px solid var(--color-border); + flex-shrink: 0; +} + +#notes-panel-title { + font-size: var(--font-size-lg); + font-weight: bold; + color: var(--color-text-primary); +} + +#notes-panel-close { + background: none; + border: none; + color: var(--color-text-secondary); + font-size: 20px; + line-height: 1; + cursor: pointer; + padding: 2px var(--space-1); + border-radius: var(--radius-sm); +} + +#notes-panel-close:hover { + color: var(--color-text-primary); + background: var(--color-bg); +} + +#notes-panel-body { + flex: 1; + padding: var(--space-4); + overflow-y: auto; + font-size: var(--font-size-base); + color: var(--color-text-primary); + line-height: 1.6; +} + +#notes-panel-body a { + color: var(--color-node-stroke); +} + +#notes-panel-body h3 { + margin-top: var(--space-4); + margin-bottom: var(--space-2); +} + +#notes-panel-body h3:first-child { + margin-top: 0; } /* === Mobile responsive === */ @@ -286,34 +392,16 @@ a { font-size: var(--font-size-lg); top: 0; position: relative; - text-align: center; padding: var(--space-2) var(--space-3); } - #header hr { - display: none; - } - - #mobile-theme-toggle { - display: inline-block; - position: absolute; + #header-actions { right: var(--space-3); - top: 50%; - transform: translateY(-50%); - background: none; - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - color: var(--color-text-secondary); - font-size: var(--font-size-sm); - font-family: var(--font-family); - padding: var(--space-1) var(--space-2); - cursor: pointer; - white-space: nowrap; } - #mobile-theme-toggle:hover { - color: var(--color-text-primary); - border-color: var(--color-text-secondary); + /* Hide desktop notes button on mobile (floating button used instead) */ + #header-notes-btn { + display: none; } #search-container { @@ -325,21 +413,12 @@ a { font-size: 13px; } - /* Hide legend and notes content on mobile */ - .legend { - display: none; - } - - #notes-content { - display: none; - } - /* Maximize SVG tree viewport */ svg { - min-height: 60vh; + height: calc(100vh - 80px); } - /* Floating notes button */ + /* Floating notes button (mobile only) */ #mobile-notes-btn { display: block; position: fixed; @@ -363,85 +442,9 @@ a { background: var(--color-bg); } - /* Notes overlay */ - #notes-overlay { - display: none; - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 299; - } - - #notes-overlay.visible { - display: block; - } - - /* Notes panel */ + /* Full-width notes panel on mobile */ #notes-panel { - display: flex; - flex-direction: column; - position: fixed; - top: 0; - right: 0; width: 100vw; - height: 100vh; - background: var(--color-surface); - z-index: 300; - transform: translateX(100%); - transition: transform 250ms ease; - will-change: transform; - } - - #notes-panel.open { - transform: translateX(0); - } - - #notes-panel-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-4); - border-bottom: 1px solid var(--color-border); - flex-shrink: 0; - } - - #notes-panel-title { - font-size: var(--font-size-lg); - font-weight: bold; - color: var(--color-text-primary); - } - - #notes-panel-close { - background: none; - border: none; - color: var(--color-text-secondary); - font-size: 20px; - line-height: 1; - cursor: pointer; - padding: 2px var(--space-1); - border-radius: var(--radius-sm); - } - - #notes-panel-close:hover { - color: var(--color-text-primary); - background: var(--color-bg); - } - - #notes-panel-body { - flex: 1; - padding: var(--space-4); - overflow-y: auto; - font-size: var(--font-size-base); - color: var(--color-text-primary); - line-height: 1.6; - } - - #notes-panel-body h3 { - margin-top: var(--space-4); - margin-bottom: var(--space-2); - } - - #notes-panel-body h3:first-child { - margin-top: 0; + box-shadow: none; } } diff --git a/public/index.html b/public/index.html index a968bdd..9fc8790 100644 --- a/public/index.html +++ b/public/index.html @@ -27,8 +27,10 @@
@@ -104,14 +106,11 @@ R - Requires registration
M - Indicates a URL that contains the search term and the URL itself must be edited manually

-
diff --git a/public/js/arf.js b/public/js/arf.js index 3dce43b..a30e5a2 100644 --- a/public/js/arf.js +++ b/public/js/arf.js @@ -84,38 +84,36 @@ d3.json("arf.json").then(function(json) { root.children.forEach(collapse); - // On mobile, stretch the viewBox to match the actual rendered aspect ratio - // so the zoom-to-fill calculation uses the real visible area, not the - // fixed 1280x800 letterboxed region. - if (window.innerWidth <= 768) { - var rect = svgEl.node().getBoundingClientRect(); - if (rect.width && rect.height) { - svgH = Math.round(svgW * (rect.height / rect.width)); - svgEl.attr("viewBox", "0 0 " + svgW + " " + svgH); - } - - // Run tree layout to get final node positions, then compute zoom from data. - tree(root); - root.descendants().forEach(function(d) { d.y = d.depth * 180; }); - var visibleNodes = root.descendants(); - var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; - visibleNodes.forEach(function(d) { - if (d.x < minX) minX = d.x; - if (d.x > maxX) maxX = d.x; - if (d.y < minY) minY = d.y; - if (d.y > maxY) maxY = d.y; - }); - var pad = 40; - var bw = (maxY - minY) || 1; - var bh = (maxX - minX) || 1; - var k = Math.min((svgW - pad * 2) / bw, (svgH - pad * 2) / bh, 3); - var cx = (minY + maxY) / 2; - var cy = (minX + maxX) / 2; - var tx = svgW / 2 - margin[3] - cx * k; - var ty = svgH / 2 - margin[0] - cy * k; - svgEl.call(zoom.transform, d3.zoomIdentity.translate(tx, ty).scale(k)); + // Stretch the viewBox to match the actual rendered aspect ratio so the + // zoom-to-fill calculation uses the real visible area, not the fixed + // 1280x800 letterboxed region. + var rect = svgEl.node().getBoundingClientRect(); + if (rect.width && rect.height) { + svgH = Math.round(svgW * (rect.height / rect.width)); + svgEl.attr("viewBox", "0 0 " + svgW + " " + svgH); } + // Run tree layout to get final node positions, then compute zoom from data. + tree(root); + root.descendants().forEach(function(d) { d.y = d.depth * 180; }); + var visibleNodes = root.descendants(); + var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; + visibleNodes.forEach(function(d) { + if (d.x < minX) minX = d.x; + if (d.x > maxX) maxX = d.x; + if (d.y < minY) minY = d.y; + if (d.y > maxY) maxY = d.y; + }); + var pad = 40; + var bw = (maxY - minY) || 1; + var bh = (maxX - minX) || 1; + var k = Math.min((svgW - pad * 2) / bw, (svgH - pad * 2) / bh, 3); + var cx = (minY + maxY) / 2; + var cy = (minX + maxX) / 2; + var tx = pad - minY * k; + var ty = svgH / 2 - margin[0] - cy * k; + svgEl.call(zoom.transform, d3.zoomIdentity.translate(tx, ty).scale(k)); + update(root); initSearch(); }); @@ -654,9 +652,16 @@ function closePanel() { if (root) update(root); } -// Keyboard: Escape closes panel +// Keyboard: Escape closes panels document.addEventListener("keydown", function(e) { - if (e.key === "Escape") closePanel(); + if (e.key === "Escape") { + var notesPanel = document.getElementById("notes-panel"); + if (notesPanel && notesPanel.classList.contains("open")) { + toggleNotesPanel(); + } else { + closePanel(); + } + } }); // Wire close button once DOM is ready @@ -808,19 +813,15 @@ function goDark() { var body = document.body; var isLight = body.classList.toggle("light-mode"); localStorage.setItem("theme", isLight ? "light" : "dark"); - var btn = document.getElementById("theme-toggle"); + var btn = document.getElementById("header-theme-toggle"); if (btn) { - btn.textContent = isLight ? "Switch to dark mode" : "Switch to light mode"; - } - var mBtn = document.getElementById("mobile-theme-toggle"); - if (mBtn) { - mBtn.textContent = isLight ? "Dark Mode" : "Light Mode"; + btn.textContent = isLight ? "Dark Mode" : "Light Mode"; } // Re-render to pick up new CSS variable values for D3 inline styles. update(root); } -// Mobile notes panel toggle +// Notes panel toggle function toggleNotesPanel() { var panel = document.getElementById("notes-panel"); var overlay = document.getElementById("notes-overlay");