Fold the mobile header into a single hamburger menu (#119)
On phones the header spanned two rows: the title block plus a control bar of the °F/°C toggle, bell, account and hamburger. Wrap the nav in a .nav-panel inside the <details> menu and inject the °F/°C toggle and account/bell into that panel instead of into the header row. On desktop the panel is display:contents, so the inline top bar renders exactly as before (order restored via flex order); on phones the panel becomes a right-anchored, viewport-clamped dropdown and the header collapses to a single row (logo + name + hamburger, tagline hidden). The viewport clamp also fixes the SEO/city pages, whose menu dropdown previously ran off-screen.
This commit is contained in:
parent
68745e16c6
commit
cde03de0f0
9 changed files with 94 additions and 39 deletions
|
|
@ -259,8 +259,12 @@ function ensureAcctEl() {
|
||||||
if (!acctEl) {
|
if (!acctEl) {
|
||||||
acctEl = document.createElement("div");
|
acctEl = document.createElement("div");
|
||||||
acctEl.className = "acct";
|
acctEl.className = "acct";
|
||||||
const anchor = brand.querySelector(".nav-menu") || brand.querySelector(".view-nav");
|
// The account/bell live inside the nav menu panel: on desktop the panel is
|
||||||
brand.insertBefore(acctEl, anchor); // top-right, just left of the nav menu
|
// display:contents so they render inline top-right; on phones they stack
|
||||||
|
// inside the hamburger dropdown. Fall back to a brand sibling if unwrapped.
|
||||||
|
const panel = brand.querySelector(".nav-panel");
|
||||||
|
if (panel) panel.appendChild(acctEl);
|
||||||
|
else brand.insertBefore(acctEl, brand.querySelector(".view-nav"));
|
||||||
}
|
}
|
||||||
return acctEl;
|
return acctEl;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
</div>
|
</div>
|
||||||
<details class="nav-menu">
|
<details class="nav-menu">
|
||||||
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
||||||
|
<div class="nav-panel">
|
||||||
<nav class="view-nav" aria-label="Views">
|
<nav class="view-nav" aria-label="Views">
|
||||||
<a href="./" data-view="map">Weekly</a>
|
<a href="./" data-view="map">Weekly</a>
|
||||||
<a href="calendar" data-view="calendar" class="active">Calendar</a>
|
<a href="calendar" data-view="calendar" class="active">Calendar</a>
|
||||||
|
|
@ -42,6 +43,7 @@
|
||||||
<a href="compare" data-view="compare">Compare</a>
|
<a href="compare" data-view="compare">Compare</a>
|
||||||
<a href="climate">Climate</a>
|
<a href="climate">Climate</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
</div>
|
</div>
|
||||||
<details class="nav-menu">
|
<details class="nav-menu">
|
||||||
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
||||||
|
<div class="nav-panel">
|
||||||
<nav class="view-nav" aria-label="Views">
|
<nav class="view-nav" aria-label="Views">
|
||||||
<a href="./" data-view="map">Weekly</a>
|
<a href="./" data-view="map">Weekly</a>
|
||||||
<a href="calendar" data-view="calendar">Calendar</a>
|
<a href="calendar" data-view="calendar">Calendar</a>
|
||||||
|
|
@ -43,6 +44,7 @@
|
||||||
<a href="compare" data-view="compare" class="active">Compare</a>
|
<a href="compare" data-view="compare" class="active">Compare</a>
|
||||||
<a href="climate">Climate</a>
|
<a href="climate">Climate</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
</div>
|
</div>
|
||||||
<details class="nav-menu">
|
<details class="nav-menu">
|
||||||
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
||||||
|
<div class="nav-panel">
|
||||||
<nav class="view-nav" aria-label="Views">
|
<nav class="view-nav" aria-label="Views">
|
||||||
<a href="./" data-view="map">Weekly</a>
|
<a href="./" data-view="map">Weekly</a>
|
||||||
<a href="calendar" data-view="calendar">Calendar</a>
|
<a href="calendar" data-view="calendar">Calendar</a>
|
||||||
|
|
@ -43,6 +44,7 @@
|
||||||
<a href="compare" data-view="compare">Compare</a>
|
<a href="compare" data-view="compare">Compare</a>
|
||||||
<a href="climate">Climate</a>
|
<a href="climate">Climate</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
</div>
|
</div>
|
||||||
<details class="nav-menu">
|
<details class="nav-menu">
|
||||||
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
||||||
|
<div class="nav-panel">
|
||||||
<nav class="view-nav" aria-label="Views">
|
<nav class="view-nav" aria-label="Views">
|
||||||
<a href="./" data-view="map" class="active">Weekly</a>
|
<a href="./" data-view="map" class="active">Weekly</a>
|
||||||
<a href="calendar" data-view="calendar">Calendar</a>
|
<a href="calendar" data-view="calendar">Calendar</a>
|
||||||
|
|
@ -46,6 +47,7 @@
|
||||||
<a href="compare" data-view="compare">Compare</a>
|
<a href="compare" data-view="compare">Compare</a>
|
||||||
<a href="climate">Climate</a>
|
<a href="climate">Climate</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
</div>
|
</div>
|
||||||
<details class="nav-menu">
|
<details class="nav-menu">
|
||||||
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
||||||
|
<div class="nav-panel">
|
||||||
<nav class="view-nav" aria-label="Views">
|
<nav class="view-nav" aria-label="Views">
|
||||||
<a href="./" data-view="map">Weekly</a>
|
<a href="./" data-view="map">Weekly</a>
|
||||||
<a href="calendar" data-view="calendar">Calendar</a>
|
<a href="calendar" data-view="calendar">Calendar</a>
|
||||||
|
|
@ -42,6 +43,7 @@
|
||||||
<a href="compare" data-view="compare">Compare</a>
|
<a href="compare" data-view="compare">Compare</a>
|
||||||
<a href="climate">Climate</a>
|
<a href="climate">Climate</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
105
static/style.css
105
static/style.css
|
|
@ -491,15 +491,23 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
|
||||||
.view-nav a:hover { color: var(--text); }
|
.view-nav a:hover { color: var(--text); }
|
||||||
.view-nav a.active { background: var(--accent); color: #fff; }
|
.view-nav a.active { background: var(--accent); color: #fff; }
|
||||||
|
|
||||||
/* Hamburger wrapper. On desktop it's transparent (display:contents) so the nav
|
/* Hamburger wrapper. On desktop the menu and its panel are transparent
|
||||||
renders inline as pills and the hamburger summary is hidden; on phones (the
|
(display:contents), so the nav pills plus the injected °F/°C toggle, bell and
|
||||||
640px block) it becomes a dropdown while the F/C toggle, bell and account stay
|
account render inline in the header row and the hamburger summary is hidden;
|
||||||
in the header bar. */
|
on phones (the 640px block) the panel becomes a dropdown behind the hamburger. */
|
||||||
.nav-menu { display: contents; }
|
.nav-menu { display: contents; }
|
||||||
/* A closed <details> hides its non-summary content; force the nav visible on
|
.nav-panel { display: contents; }
|
||||||
|
/* A closed <details> hides its non-summary content; force the panel visible on
|
||||||
desktop (the 640px block re-hides it behind the hamburger on phones). */
|
desktop (the 640px block re-hides it behind the hamburger on phones). */
|
||||||
.nav-menu > .view-nav { display: inline-flex; }
|
.nav-menu > .view-nav,
|
||||||
|
.nav-panel > .view-nav { display: inline-flex; }
|
||||||
.nav-menu::details-content { display: contents; }
|
.nav-menu::details-content { display: contents; }
|
||||||
|
/* The controls are injected into the panel after the nav in DOM order; restore
|
||||||
|
the header's visual order — °F/°C · account · tabs — on desktop. On phones the
|
||||||
|
640px block resets these so the dropdown stacks nav → °F/°C → account. */
|
||||||
|
.nav-panel > .unit-toggle { order: 1; }
|
||||||
|
.nav-panel > .acct { order: 2; }
|
||||||
|
.nav-panel > .view-nav { order: 3; }
|
||||||
.nav-toggle { display: none; list-style: none; cursor: pointer; }
|
.nav-toggle { display: none; list-style: none; cursor: pointer; }
|
||||||
.nav-toggle::-webkit-details-marker { display: none; }
|
.nav-toggle::-webkit-details-marker { display: none; }
|
||||||
.nav-toggle svg { display: block; }
|
.nav-toggle svg { display: block; }
|
||||||
|
|
@ -1308,43 +1316,70 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
|
||||||
|
|
||||||
header { padding: 14px 16px; }
|
header { padding: 14px 16px; }
|
||||||
h1 { font-size: 19px; }
|
h1 { font-size: 19px; }
|
||||||
.tag { font-size: 12px; }
|
/* Drop the tagline on phones so the header stays a tight single row. */
|
||||||
|
.tag { display: none; }
|
||||||
|
|
||||||
/* The title (logo + name + tagline) claims the whole first row so its text never
|
/* One compact row: logo + name on the left, the hamburger flush-right. The nav,
|
||||||
collapses to a one-word-per-line sliver; the F/C toggle, bell, account and
|
°F/°C toggle, bell and account all fold into the hamburger dropdown below, so
|
||||||
hamburger wrap to a control bar on the row below. (A lone hamburger — the SEO
|
the header never grows a second control-bar row. */
|
||||||
pages, which have no toggle/account — is small enough to stay on the title row.) */
|
.brand { flex-wrap: nowrap; align-items: center; gap: 10px; }
|
||||||
.brand { flex-wrap: wrap; align-items: flex-start; row-gap: 12px; }
|
.brand > div:not(.unit-toggle):not(.acct) { flex: 1 1 auto; min-width: 0; }
|
||||||
.brand > div:not(.unit-toggle):not(.acct) { flex: 1 1 0; min-width: calc(100% - 92px); }
|
.logo { align-self: center; margin-top: 0; }
|
||||||
.logo { align-self: flex-start; margin-top: 3px; }
|
|
||||||
/* The view navigation collapses into a hamburger dropdown; the F/C toggle, bell
|
/* A real box again (not display:contents) so it anchors the absolute dropdown
|
||||||
and account sit in the control bar beside it. */
|
and holds the hamburger, pinned to the header's right edge. */
|
||||||
/* A real box again (not display:contents) so it's the positioning context for
|
.nav-menu { display: inline-flex; position: relative; align-self: center; flex: 0 0 auto; }
|
||||||
the absolute dropdown and holds the hamburger. */
|
|
||||||
.nav-menu { display: inline-flex; position: relative; align-self: center; }
|
|
||||||
.nav-toggle {
|
.nav-toggle {
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
width: 44px; height: 40px; border: 1px solid var(--border); border-radius: 10px;
|
width: 44px; height: 44px; border: 1px solid var(--border); border-radius: 12px;
|
||||||
background: var(--surface-2); color: var(--text);
|
background: var(--surface-2); color: var(--text);
|
||||||
}
|
}
|
||||||
.nav-menu[open] .nav-toggle { border-color: var(--accent); color: var(--accent); }
|
.nav-menu[open] .nav-toggle { border-color: var(--accent); color: var(--accent); }
|
||||||
.view-nav {
|
|
||||||
position: absolute; right: 0; top: calc(100% + 8px); z-index: 60; margin: 0;
|
/* The panel is the dropdown: right-anchored and clamped to the viewport so it
|
||||||
flex-direction: column; align-items: stretch; width: max-content; min-width: 176px;
|
never runs off-screen (this also fixes the SEO/city pages, whose only panel
|
||||||
box-shadow: 0 12px 30px rgba(0, 0, 0, .35);
|
item is the nav). Contents stack nav → °F/°C → account top-to-bottom. */
|
||||||
|
.nav-panel {
|
||||||
|
display: flex; flex-direction: column; align-items: stretch; gap: 8px;
|
||||||
|
position: absolute; right: 0; top: calc(100% + 8px); z-index: 60;
|
||||||
|
margin: 0; padding: 10px; width: max-content; min-width: 224px;
|
||||||
|
max-width: calc(100vw - 28px);
|
||||||
|
background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
|
||||||
|
box-shadow: 0 16px 40px rgba(0, 0, 0, .45);
|
||||||
}
|
}
|
||||||
.nav-menu:not([open]) .view-nav { display: none; }
|
.nav-menu:not([open]) .nav-panel { display: none; }
|
||||||
.view-nav a { min-height: 44px; padding: 10px 14px; justify-content: flex-start; }
|
/* Reset the desktop cluster order — the dropdown wants plain DOM order. */
|
||||||
|
.nav-panel > .unit-toggle, .nav-panel > .acct, .nav-panel > .view-nav { order: 0; }
|
||||||
|
|
||||||
|
/* Nav links: a full-width vertical list, hairline-separated from the controls
|
||||||
|
below (the :not(:last-child) keeps SEO pages — nav only — divider-free). */
|
||||||
|
.view-nav {
|
||||||
|
display: flex; flex-direction: column; align-items: stretch; gap: 2px; margin: 0;
|
||||||
|
background: transparent; border: 0; border-radius: 0; padding: 0;
|
||||||
|
}
|
||||||
|
.nav-panel > .view-nav:not(:last-child) {
|
||||||
|
padding-bottom: 8px; margin-bottom: 2px; border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.view-nav a { min-height: 44px; padding: 11px 12px; justify-content: flex-start; border-radius: 9px; }
|
||||||
.view-nav a + a::before { display: none; }
|
.view-nav a + a::before { display: none; }
|
||||||
/* Collapse the account entry to an icon-only button so it doesn't crush the
|
|
||||||
title/tagline on narrow screens (it still opens the modal / menu). */
|
/* °F/°C toggle: full-width segmented control inside the dropdown. */
|
||||||
.acct-btn span { display: none; }
|
.nav-panel > .unit-toggle { margin: 0; width: 100%; align-self: stretch; }
|
||||||
.acct-btn { padding: 8px; min-width: 40px; justify-content: center; }
|
.nav-panel > .unit-toggle button { flex: 1; }
|
||||||
/* The bell sits left of the account button, so a wide dropdown anchored to the
|
|
||||||
bell (.notif) overflowed off the left of the screen. Drop .notif's positioning
|
/* Account row (bell + account/Sign in) spans the dropdown; the account button
|
||||||
context so the dropdown anchors to the .acct cluster at the header's right
|
keeps its label and fills, the bell stays a fixed square. Popovers open below,
|
||||||
edge instead, keeping it fully on-screen. */
|
clamped to the viewport so the notification list never overflows. */
|
||||||
.notif { position: static; }
|
.nav-panel > .acct { margin: 0; width: 100%; align-self: stretch; }
|
||||||
|
.nav-panel > .acct .acct-menu { flex: 1; }
|
||||||
|
.nav-panel > .acct .acct-btn:not(.notif-btn) { width: 100%; justify-content: center; }
|
||||||
|
/* The bell's notification list would anchor to the small bell button and spill
|
||||||
|
off the left edge; drop .notif's positioning context so it anchors to the
|
||||||
|
panel instead — dropping below the menu, right-aligned and within the
|
||||||
|
viewport. The account menu already fits; just clamp its width. */
|
||||||
|
.nav-panel .notif { position: static; }
|
||||||
|
.nav-panel .notif-pop { right: 0; width: min(340px, calc(100vw - 24px)); }
|
||||||
|
.acct-pop { max-width: calc(100vw - 40px); }
|
||||||
|
|
||||||
main { padding: 14px 14px 40px; }
|
main { padding: 14px 14px 40px; }
|
||||||
.controls { gap: 12px; margin-bottom: 14px; }
|
.controls { gap: 12px; margin-bottom: 14px; }
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
<details class="nav-menu">
|
<details class="nav-menu">
|
||||||
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
||||||
|
<div class="nav-panel">
|
||||||
<nav class="view-nav" aria-label="Views">
|
<nav class="view-nav" aria-label="Views">
|
||||||
<a href="./" data-view="map">Weekly</a>
|
<a href="./" data-view="map">Weekly</a>
|
||||||
<a href="calendar" data-view="calendar">Calendar</a>
|
<a href="calendar" data-view="calendar">Calendar</a>
|
||||||
|
|
@ -40,6 +41,7 @@
|
||||||
<a href="compare" data-view="compare">Compare</a>
|
<a href="compare" data-view="compare">Compare</a>
|
||||||
<a href="climate">Climate</a>
|
<a href="climate">Climate</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,11 @@ function syncUnitToggle(wrap) {
|
||||||
const b = e.target.closest("button[data-unit]");
|
const b = e.target.closest("button[data-unit]");
|
||||||
if (b) setUnit(b.dataset.unit);
|
if (b) setUnit(b.dataset.unit);
|
||||||
});
|
});
|
||||||
const anchor = brand.querySelector(".nav-menu") || brand.querySelector(".view-nav");
|
// Lives inside the nav menu panel: display:contents keeps it inline top-right
|
||||||
brand.insertBefore(wrap, anchor); // sits just left of the nav (top-right)
|
// on desktop; on phones it stacks inside the hamburger dropdown. Fall back to a
|
||||||
|
// brand sibling if the panel isn't present.
|
||||||
|
const panel = brand.querySelector(".nav-panel");
|
||||||
|
if (panel) panel.appendChild(wrap);
|
||||||
|
else brand.insertBefore(wrap, brand.querySelector(".view-nav"));
|
||||||
syncUnitToggle(wrap);
|
syncUnitToggle(wrap);
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue