Browse Source

offcanvas nav

windhamdavid 1 week ago
parent
commit
1aa69dc822
3 changed files with 169 additions and 6 deletions
  1. 4 6
      docusaurus.config.js
  2. 93 0
      src/clientModules/nav-underline.js
  3. 72 0
      src/css/custom.css

+ 4 - 6
docusaurus.config.js

@@ -18,7 +18,7 @@ export default {
     },
   },
   themes: ['@docusaurus/theme-mermaid'],
-  clientModules: ['./src/clientModules/ask-widget.js'],
+  clientModules: ['./src/clientModules/ask-widget.js', './src/clientModules/nav-underline.js'],
   plugins: [
     'plugin-image-zoom',
     require.resolve('docusaurus-plugin-matomo'),
@@ -143,12 +143,10 @@ export default {
         {to: 'help', label: 'Help', position: 'left'},
         {type: 'search', position: 'left'},
         {
-          href: 'https://davidawindham.com/desk',
-          label: 'David A. Windham',
-          'aria-label': 'David A. Windham',
-          className: 'header-homepage-link',
-          target: '_self',
+          type: 'html',
           position: 'right',
+          value:
+            '<a class="header-homepage-link dw-brand" role="button" tabindex="0" aria-haspopup="true" aria-expanded="false" aria-controls="dw-nav" title="David A. Windham">David A. Windham</a>',
         },
       ],
     },

+ 93 - 0
src/clientModules/nav-underline.js

@@ -0,0 +1,93 @@
+/**
+ * Site offcanvas top-nav — mirrors the parent site (davidawindham.com).
+ * The "David A. Windham" brand (a .dw-brand in the navbar) opens a slide-down
+ * bar with About/Desk/Studio/Work and a "magic line" underline that follows the
+ * hovered item. Injected at <body> level (not inside the navbar) because the
+ * navbar's hideOnScroll transform would otherwise trap the fixed-position panel.
+ * Toggled by JS (click/Enter/Space, backdrop, Esc, or a #nav deep-link) rather
+ * than :target, which keeps it robust across Docusaurus SPA navigation and
+ * avoids a broken-anchor on #nav. Event delegation on document survives navbar
+ * re-renders.
+ */
+if (typeof document !== 'undefined') {
+  const OFFCANVAS_HTML =
+    '<div class="dw-offcanvas" id="dw-nav" role="dialog" aria-label="Site navigation">' +
+      '<nav class="dw-offcanvas-nav">' +
+        '<ul class="dw-nav">' +
+          '<li><a href="https://davidawindham.com/about">About</a></li>' +
+          '<li><a href="https://davidawindham.com/desk">Desk</a></li>' +
+          '<li><a href="https://davidawindham.com/studio">Studio</a></li>' +
+          '<li><a href="https://davidawindham.com/work">Work</a></li>' +
+          '<span class="dw-nav-underline" aria-hidden="true"></span>' +
+        '</ul>' +
+      '</nav>' +
+    '</div>' +
+    '<a href="#" class="dw-offcanvas-backdrop" tabindex="-1" aria-hidden="true"></a>';
+
+  function ensureInjected() {
+    if (document.getElementById('dw-nav')) return;
+    const root = document.createElement('div');
+    root.className = 'dw-offcanvas-root';
+    root.innerHTML = OFFCANVAS_HTML;
+    document.body.appendChild(root);
+  }
+  const panel = () => document.getElementById('dw-nav');
+  const isOpen = () => { const o = panel(); return !!o && o.classList.contains('dw-open'); };
+  function setOpen(open) {
+    ensureInjected();
+    const o = panel();
+    if (!o) return;
+    o.classList.toggle('dw-open', open);
+    document.querySelectorAll('.dw-brand').forEach((b) => b.setAttribute('aria-expanded', String(open)));
+  }
+
+  document.addEventListener('click', (e) => {
+    const t = e.target;
+    if (!t || !t.closest) return;
+    if (t.closest('.dw-brand')) { e.preventDefault(); setOpen(!isOpen()); return; }
+    if (t.closest('.dw-offcanvas-backdrop')) { e.preventDefault(); setOpen(false); return; }
+    if (t.closest('.dw-offcanvas .dw-nav a')) setOpen(false); // let the link navigate away
+  });
+  document.addEventListener('keydown', (e) => {
+    if (e.key === 'Escape' && isOpen()) { setOpen(false); return; }
+    const t = e.target;
+    if (t && t.closest && t.closest('.dw-brand') && (e.key === 'Enter' || e.key === ' ')) {
+      e.preventDefault();
+      setOpen(!isOpen());
+    }
+  });
+
+  // magic-line underline
+  function positionUnderline(a) {
+    const ul = a.closest('ul.dw-nav');
+    if (!ul) return;
+    const line = ul.querySelector('.dw-nav-underline');
+    const li = a.closest('li');
+    if (!line || !li) return;
+    line.style.left = li.offsetLeft + 'px';
+    line.style.width = li.offsetWidth + 'px';
+    line.style.top = li.offsetTop + li.offsetHeight + 'px';
+    line.style.opacity = '1';
+  }
+  document.addEventListener('mouseover', (e) => {
+    const t = e.target;
+    const a = t && t.closest && t.closest('ul.dw-nav li a');
+    if (a) positionUnderline(a);
+  });
+  document.addEventListener('mouseout', (e) => {
+    const t = e.target;
+    const ul = t && t.closest && t.closest('ul.dw-nav');
+    if (ul && (!e.relatedTarget || !ul.contains(e.relatedTarget))) {
+      const line = ul.querySelector('.dw-nav-underline');
+      if (line) line.style.opacity = '0';
+    }
+  });
+
+  function boot() {
+    ensureInjected();
+    if (window.location.hash === '#nav') setOpen(true); // deep-link to open
+  }
+  window.addEventListener('hashchange', () => { if (window.location.hash === '#nav') setOpen(true); });
+  if (document.readyState !== 'loading') boot();
+  else document.addEventListener('DOMContentLoaded', boot);
+}

+ 72 - 0
src/css/custom.css

@@ -124,6 +124,10 @@ table td img {
 .title_f1Hy {
   font-size: 1.5rem !important;
 }
+/* A bit more breathing room above the Posts (blog) h1 title */
+main[itemtype*="schema.org/Blog"] h1 {
+  margin-top: 1.5rem;
+}
 article[itemprop="blogPost"] footer {
   margin-top: 1rem !important;
 }
@@ -294,4 +298,72 @@ form input {
 [data-theme='light'] .navbar .navbar__link,
 [data-theme='light'] .navbar .header-homepage-link {
   color: #e6e6e6;
+}
+
+/* ===== Offcanvas top-nav (#dw-nav) — mirrors the parent site's slide-down nav.
+   .dw-brand (in the navbar) toggles .dw-open via src/clientModules/nav-underline.js;
+   the panel + backdrop are injected at <body> so the navbar's hideOnScroll
+   transform doesn't trap the fixed panel. ===== */
+.dw-brand { cursor: pointer; color: #e6e6e6; }
+.dw-brand:hover { color: #25c2a0; }
+.dw-offcanvas {
+  position: fixed;
+  top: 0; left: 0; right: 0;
+  z-index: 1045; /* above the Docusaurus navbar */
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: var(--ifm-navbar-height, 60px);
+  padding: 0.5rem 1rem;
+  visibility: hidden;
+  transform: translateY(-100%);
+  transition: transform 0.3s ease-in-out, visibility 0.3s;
+  background: rgba(72, 76, 87, 0.94); /* footer color (#484c57), translucent */
+  backdrop-filter: blur(6px);
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
+}
+.dw-offcanvas.dw-open { visibility: visible; transform: none; }
+.dw-offcanvas-backdrop {
+  display: none;
+  position: fixed;
+  inset: 0;
+  z-index: 1040; /* below the panel */
+  background: rgba(0, 0, 0, 0.4);
+}
+.dw-offcanvas.dw-open ~ .dw-offcanvas-backdrop { display: block; }
+.dw-offcanvas-nav { display: flex; justify-content: center; width: 100%; }
+ul.dw-nav {
+  position: relative;
+  display: flex;
+  gap: 1.75rem;
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+ul.dw-nav li { margin: 0; }
+ul.dw-nav li a {
+  color: #ffffff;
+  font-family: 'Playfair Display', serif;
+  font-weight: 700;
+  font-size: 1.35rem;
+  line-height: 1.2;
+  text-decoration: none;
+}
+ul.dw-nav li a:hover { color: #25c2a0; }
+.dw-nav-underline {
+  position: absolute;
+  top: 0; left: 0;
+  width: 0; height: 2px;
+  background: #25c2a0;
+  border-radius: 1px;
+  opacity: 0;
+  pointer-events: none;
+  transition: left 0.4s cubic-bezier(0.785, 0.135, 0.15, 0.86),
+              width 0.4s cubic-bezier(0.785, 0.135, 0.15, 0.86),
+              top 0.4s cubic-bezier(0.785, 0.135, 0.15, 0.86),
+              opacity 0.3s ease;
+}
+@media (prefers-reduced-motion: reduce) {
+  .dw-offcanvas { transition: none; }
+  .dw-nav-underline { transition: opacity 0.3s ease; }
 }