diff options
| author | grm <grm@eyesin.space> | 2026-09-18 19:29:30 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-18 19:29:30 +0300 |
| commit | 63f9fdff6e5bdf6333a4b7813ebeb5b797838b6d (patch) | |
| tree | 73e2831905e5a8bec826c4ffa4a6531313f40a98 /internal | |
| parent | 0a248d9a0e6ef6439fca25aa854968b6b5dac2ef (diff) | |
| download | blogspace-63f9fdff6e5bdf6333a4b7813ebeb5b797838b6d.tar.gz blogspace-63f9fdff6e5bdf6333a4b7813ebeb5b797838b6d.tar.bz2 blogspace-63f9fdff6e5bdf6333a4b7813ebeb5b797838b6d.zip | |
Fold the header menu into a button when its links do not fit
A long menu wrapped onto two or three lines on phones and narrow
windows. With the new Design switch the header menu folds into a ☰
button (an icon, the word Menu, or both) carried by the title or logo
block just above it, at the right end of its row, or by the menu bar
when nothing is above it; the button opens the links as a stacked list
with the search box on top.
The open/closed state is a hidden checkbox and the button its label,
so opening needs no script and works in old browsers. Whether to fold
is decided by a small inline script — any link on a second line means
the menu does not fit — re-checked on resize; without it blog.css folds
on phones only, and browsers too old for that keep the wrapping menu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/i18n/el.go | 12 | ||||
| -rw-r--r-- | internal/web/layout.go | 18 | ||||
| -rw-r--r-- | internal/web/static/blog.css | 47 | ||||
| -rw-r--r-- | internal/web/templates/dashboard/design.html | 11 | ||||
| -rw-r--r-- | internal/web/templates/layouts/blog.html | 46 | ||||
| -rw-r--r-- | internal/web/theme.go | 11 | ||||
| -rw-r--r-- | internal/web/web_test.go | 76 |
7 files changed, 205 insertions, 16 deletions
diff --git a/internal/i18n/el.go b/internal/i18n/el.go index 2afef53..66446f1 100644 --- a/internal/i18n/el.go +++ b/internal/i18n/el.go @@ -397,10 +397,14 @@ var el = map[string]string{ "Underlined": "Υπογραμμισμένη", "Boxed": "Σε πλαίσιο", "Not marked": "Χωρίς σήμανση", - "Uppercase, spaced": "Κεφαλαία, αραιά", - "Show the date on posts": "Εμφάνιση ημερομηνίας στις δημοσιεύσεις", - "Show tags with a # in front": "Εμφάνιση των ετικετών με # μπροστά", - "Rule under titles": "Γραμμή κάτω από τους τίτλους", + "When the menu does not fit": "Όταν το μενού δεν χωράει", + "Fold the header menu into a button when its links do not fit on one line": "Δίπλωμα του μενού της κεφαλίδας σε κουμπί όταν οι σύνδεσμοί του δεν χωρούν σε μία γραμμή", + "Button": "Κουμπί", + "The button sits at the right of the title or logo just above the menu, or in the menu bar itself when nothing is above it, and opens the links as a list. Readers whose browser runs no scripts get it on phones.": "Το κουμπί μπαίνει δεξιά από τον τίτλο ή το λογότυπο ακριβώς πάνω από το μενού, ή μέσα στη μπάρα του μενού όταν δεν υπάρχει τίποτα από πάνω, και ανοίγει τους συνδέσμους σε λίστα. Όσοι αναγνώστες έχουν πρόγραμμα περιήγησης χωρίς scripts το βλέπουν στα κινητά.", + "Uppercase, spaced": "Κεφαλαία, αραιά", + "Show the date on posts": "Εμφάνιση ημερομηνίας στις δημοσιεύσεις", + "Show tags with a # in front": "Εμφάνιση των ετικετών με # μπροστά", + "Rule under titles": "Γραμμή κάτω από τους τίτλους", "The rule goes under page and post titles, in the link colour.": "Η γραμμή μπαίνει κάτω από τους τίτλους σελίδων και δημοσιεύσεων, στο χρώμα των συνδέσμων.", "Show announcements on post pages": "Εμφάνιση ανακοινώσεων στις σελίδες δημοσιεύσεων", "each page has its own switch": "κάθε σελίδα έχει τον δικό της διακόπτη", diff --git a/internal/web/layout.go b/internal/web/layout.go index 91728ed..d201758 100644 --- a/internal/web/layout.go +++ b/internal/web/layout.go @@ -130,6 +130,12 @@ type Layout struct { // A header search box sits in the menu row when there is one, so the two // header modules need to know about each other. HeaderMenu, HeaderSearch bool + // Fold says the header menu folds into a button (theme nav_fold, and a + // menu in the header); BurgerAt is the index in Header of the module that + // carries the button: the title or logo block just before the menu, so it + // sits at the right of the brand, else the menu bar itself. + Fold bool + BurgerAt int } // buildLayout groups modules by area, dropping areas the theme has switched off. @@ -156,6 +162,18 @@ func buildLayout(t Theme, mods []store.Module) Layout { l.Footer = append(l.Footer, m) } } + if t.NavFold { + for j, m := range l.Header { + if m.Kind != "menu" { + continue + } + l.Fold, l.BurgerAt = true, j + if j > 0 && (l.Header[j-1].Kind == "title" || l.Header[j-1].Kind == "logo" && t.Logo != "") { // a logo module without a logo renders nothing + l.BurgerAt = j - 1 + } + break + } + } l.HasLeft = t.KeepColumns || len(l.Left) > 0 l.HasRight = t.KeepColumns || len(l.Right) > 0 return l diff --git a/internal/web/static/blog.css b/internal/web/static/blog.css index 7b91dbe..b3b0312 100644 --- a/internal/web/static/blog.css +++ b/internal/web/static/blog.css @@ -49,6 +49,53 @@ img { max-width: 100%; height: auto; } .nav-spread .site-header .nav-inner { text-align: justify; line-height: 0; } .nav-spread .site-header .nav-inner > a, .nav-spread .site-header .search-form { line-height: 1.5; } .nav-spread .site-header .nav-inner::after { content: ""; display: inline-block; width: 100%; } +/* menu fold (theme nav_fold): the header menu becomes a button when its links + do not fit. The hidden (offscreen, still focusable) checkbox before the + header's blocks is the open/closed state; the label is the button, at the + right of the title or logo block above the menu (.has-burger on that + block), or in the bar when nothing is above it. Which state applies: + .nav-folded is set by the page's script when a link wraps, .nav-auto is the + no-script fallback (folded on phones only) the script removes; the two + blocks below must stay identical. */ +.nav-toggle { position: absolute; left: -9999px; } +.nav-burger { display: none; cursor: pointer; font-weight: bold; font-size: 1.2em; line-height: 2em; padding: 0 0.5em; white-space: nowrap; } +.burger-icon .nb-text, .burger-text .nb-icon { display: none; } +.burger-both .nb-icon { margin-right: 0.3em; } +.nav-toggle:focus-visible ~ .has-burger .nav-burger { outline: 2px solid; } /* the keyboard's focus ring, on the button */ +.nav-folded .nav-burger { display: block; } +.nav-folded .has-burger { position: relative; padding-right: 3.6em; } +.nav-folded.header-center .has-burger { padding-left: 3.6em; } /* the brand stays centred */ +.nav-folded .has-burger > .nav-burger { position: absolute; right: 1em; top: 50%; margin-top: -1em; } +.nav-folded .site-nav.has-burger { padding-right: 0; } +.nav-folded .site-nav.has-burger .nav-inner { padding-left: 0; padding-right: 0; } /* the button keeps its place when the panel opens */ +.nav-folded .site-nav.has-burger .nav-inner > .nav-burger { display: inline-block; margin-left: 0.5em; } +.nav-folded .site-nav .nav-inner > a, .nav-folded .site-nav .search-form { display: none; } +.nav-folded .site-nav:not(.has-burger) { border-bottom: 0; } /* the bar is nothing until opened */ +.nav-folded .site-nav:not(.has-burger) .nav-inner { padding: 0; } +.nav-folded .nav-toggle:checked ~ .site-nav { border-bottom: 1px solid rgba(0,0,0,0.1); } +.nav-folded .nav-toggle:checked ~ .site-nav .nav-inner { padding: 0.3em 0; text-align: left; line-height: 1.5; } +.nav-folded .nav-toggle:checked ~ .site-nav .nav-inner > a { display: block; padding: 0.6em 1.2em; } +.nav-folded .nav-toggle:checked ~ .site-nav .nav-inner::after { display: none; } +.nav-folded .nav-toggle:checked ~ .site-nav .search-form { display: block; float: none; margin: 0.4em 1.2em; } +.nav-folded .nav-toggle:checked ~ .site-nav .search-form input { width: 100%; } +@media (max-width: 700px) { + .nav-auto .nav-burger { display: block; } + .nav-auto .has-burger { position: relative; padding-right: 3.6em; } + .nav-auto.header-center .has-burger { padding-left: 3.6em; } + .nav-auto .has-burger > .nav-burger { position: absolute; right: 1em; top: 50%; margin-top: -1em; } + .nav-auto .site-nav.has-burger { padding-right: 0; } + .nav-auto .site-nav.has-burger .nav-inner { padding-left: 0; padding-right: 0; } /* the button keeps its place when the panel opens */ + .nav-auto .site-nav.has-burger .nav-inner > .nav-burger { display: inline-block; margin-left: 0.5em; } + .nav-auto .site-nav .nav-inner > a, .nav-auto .site-nav .search-form { display: none; } + .nav-auto .site-nav:not(.has-burger) { border-bottom: 0; } + .nav-auto .site-nav:not(.has-burger) .nav-inner { padding: 0; } + .nav-auto .nav-toggle:checked ~ .site-nav { border-bottom: 1px solid rgba(0,0,0,0.1); } + .nav-auto .nav-toggle:checked ~ .site-nav .nav-inner { padding: 0.3em 0; text-align: left; line-height: 1.5; } + .nav-auto .nav-toggle:checked ~ .site-nav .nav-inner > a { display: block; padding: 0.6em 1.2em; } + .nav-auto .nav-toggle:checked ~ .site-nav .nav-inner::after { display: none; } + .nav-auto .nav-toggle:checked ~ .site-nav .search-form { display: block; float: none; margin: 0.4em 1.2em; } + .nav-auto .nav-toggle:checked ~ .site-nav .search-form input { width: 100%; } +} .body-wrap { padding: 1.5em 1em; } .content { padding: 1.5em 2em; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.15); } .page-title { margin-top: 0; } diff --git a/internal/web/templates/dashboard/design.html b/internal/web/templates/dashboard/design.html index ec4f70d..0b27e17 100644 --- a/internal/web/templates/dashboard/design.html +++ b/internal/web/templates/dashboard/design.html @@ -187,6 +187,17 @@ <label for="nav-hover-custom" class="check">{{t "Change colour when the mouse is over a link"}}</label> <label class="hover-color">{{t "Hover colour"}}<br><input type="color" name="nav_hover" value="{{if $t.NavHover}}{{$t.NavHover}}{{else}}{{$t.NavText}}{{end}}"></label> </div> + <h3>{{t "When the menu does not fit"}}</h3> + <div class="hoverpick"> + <input type="checkbox" name="nav_fold" id="nav-fold"{{if $t.NavFold}} checked{{end}}> + <label for="nav-fold" class="check">{{t "Fold the header menu into a button when its links do not fit on one line"}}</label> + <label class="hover-color">{{t "Button"}}<br><select name="nav_fold_label"> + <option value="icon"{{if eq $t.NavFoldLabel "icon"}} selected{{end}}>☰</option> + <option value="text"{{if eq $t.NavFoldLabel "text"}} selected{{end}}>{{t "Menu"}}</option> + <option value="both"{{if eq $t.NavFoldLabel "both"}} selected{{end}}>☰ {{t "Menu"}}</option> + </select></label> + </div> + <p class="muted small">{{t "The button sits at the right of the title or logo just above the menu, or in the menu bar itself when nothing is above it, and opens the links as a list. Readers whose browser runs no scripts get it on phones."}}</p> </div> <div class="card" id="content"> diff --git a/internal/web/templates/layouts/blog.html b/internal/web/templates/layouts/blog.html index 7ee0b54..d55512a 100644 --- a/internal/web/templates/layouts/blog.html +++ b/internal/web/templates/layouts/blog.html @@ -13,10 +13,7 @@ </head> {{$l := .Data.layout}} <body class="{{if $l.HasLeft}}has-left {{end}}{{if $l.HasRight}}has-right {{end}}{{.Data.theme.NavClass}}{{with .Data.theme.TitleRuleClass}} {{.}}{{end}}"> -{{if or $l.Header .Data.theme.HeaderImage}}{{if .Data.theme.HeaderOn}}<div class="site-header"> - {{if .Data.theme.HeaderImage}}<div class="header-image"><a href="/"><img src="/media/{{.Data.theme.HeaderImage}}" alt=""></a></div>{{end}} - {{range $l.Header}}{{template "module" (dict "m" . "root" $)}}{{end}} -</div>{{end}}{{end}} +{{if or $l.Header .Data.theme.HeaderImage}}{{if .Data.theme.HeaderOn}}{{template "siteheader" .}}{{end}}{{end}} <div class="wrap body-wrap cols"> {{$n := .Data.notices}} <div class="col col-main"><div class="content"> @@ -43,6 +40,37 @@ </body> </html>{{end}} +{{/* The header: the banner, then its modules in order. With the menu folding + (Layout.Fold) a hidden checkbox comes first: it is the open/closed state, + so the burger label toggles the menu with no script and in old browsers + (`:checked ~` reaches every block after it). Folding itself is decided by + the script below — any link on a second line means the menu does not fit — + which drops nav-auto, the no-script fallback blog.css folds on phones. */}} +{{define "siteheader"}}{{$l := .Data.layout}}{{$t := .Data.theme}}<div class="site-header{{if $l.Fold}} nav-fold nav-auto{{end}}{{if eq $t.HeaderAlign "center"}} header-center{{end}}"> + {{if $l.Fold}}<input type="checkbox" id="nav-toggle" class="nav-toggle" aria-label="{{t "Menu"}}">{{end}} + {{if $t.HeaderImage}}<div class="header-image"><a href="/"><img src="/media/{{$t.HeaderImage}}" alt=""></a></div>{{end}} + {{range $i, $m := $l.Header}}{{template "module" (dict "m" $m "root" $ "burger" (and $l.Fold (eq $i $l.BurgerAt)))}}{{end}} +</div>{{if $l.Fold}}<script>(function () { + var h = document.querySelector && document.querySelector(".site-header.nav-fold"), cb = document.getElementById("nav-toggle"); + if (!h || !h.classList || !window.addEventListener) return; // old browsers keep the phone-only fallback + var nav = h.querySelector(".site-nav .nav-inner"), links = nav && nav.getElementsByTagName("a"), timer; + if (!links || !links.length) return; + h.classList.remove("nav-auto"); + function fit() { // unfold, look, refold before anything is painted + h.classList.remove("nav-folded"); + var top = links[0].offsetTop, wrapped = false; + for (var i = 1; i < links.length && !wrapped; i++) wrapped = links[i].offsetTop !== top; + if (wrapped) h.classList.add("nav-folded"); + } + fit(); + window.addEventListener("load", fit); // web fonts change the widths + window.addEventListener("resize", function () { clearTimeout(timer); timer = setTimeout(fit, 100); }); + if (cb) cb.addEventListener("change", function () { cb.setAttribute("aria-expanded", cb.checked ? "true" : "false"); }); +})();</script>{{end}}{{end}} + +{{/* The fold button, a label for the checkbox above. */}} +{{define "burger"}}<label for="nav-toggle" class="nav-burger burger-{{.NavFoldLabel}}"><span class="nb-icon" aria-hidden="true">☰</span><span class="nb-text">{{t "Menu"}}</span></label>{{end}} + {{define "notices"}}{{if .}}<div class="notices"> {{range .}}<aside class="notice notice-{{.Style}}{{if .Title}} headed{{end}}{{if .Width}} w-{{.Width}}{{end}}"> {{if .Title}}<h3 class="notice-title">{{.Title}}</h3>{{end}} @@ -53,14 +81,14 @@ {{/* One layout module. .m is the store.Module, .root the page data; the same kind renders differently in the header (big blocks) and elsewhere (boxes). */}} -{{define "module"}}{{$m := .m}}{{$r := .root}}{{$t := $r.Data.theme}}{{$rc := $t.RuleClass}} -{{- if eq $m.Kind "title"}}<div class="wrap header-text{{with $rc}} {{.}}{{end}}">{{template "rule" (dict "before" true "rc" $rc "theme" $t)}}<div class="brand"> +{{define "module"}}{{$m := .m}}{{$r := .root}}{{$t := $r.Data.theme}}{{$rc := $t.RuleClass}}{{$b := .burger}} +{{- if eq $m.Kind "title"}}<div class="wrap header-text{{with $rc}} {{.}}{{end}}{{if $b}} has-burger{{end}}">{{template "rule" (dict "before" true "rc" $rc "theme" $t)}}<div class="brand"> <h1 class="site-title"><a href="/">{{$r.Blog.Title}}</a></h1> {{if $r.Blog.Tagline}}<p class="tagline">{{$r.Blog.Tagline}}</p>{{end}} -</div>{{template "rule" (dict "before" false "rc" $rc "theme" $t)}}</div> -{{- else if eq $m.Kind "logo"}}{{if $t.Logo}}{{if eq $m.Area "header"}}<div class="wrap header-logo{{with $rc}} {{.}}{{end}}">{{template "rule" (dict "before" true "rc" $rc "theme" $t)}}<div class="brand"><a href="/"><img class="logo" src="/media/{{$t.Logo}}" alt="{{$r.Blog.Title}}"></a></div>{{template "rule" (dict "before" false "rc" $rc "theme" $t)}}</div> +</div>{{template "rule" (dict "before" false "rc" $rc "theme" $t)}}{{if $b}}{{template "burger" $t}}{{end}}</div> +{{- else if eq $m.Kind "logo"}}{{if $t.Logo}}{{if eq $m.Area "header"}}<div class="wrap header-logo{{with $rc}} {{.}}{{end}}{{if $b}} has-burger{{end}}">{{template "rule" (dict "before" true "rc" $rc "theme" $t)}}<div class="brand"><a href="/"><img class="logo" src="/media/{{$t.Logo}}" alt="{{$r.Blog.Title}}"></a></div>{{template "rule" (dict "before" false "rc" $rc "theme" $t)}}{{if $b}}{{template "burger" $t}}{{end}}</div> {{else}}<div class="mod mod-logo"><a href="/"><img class="logo" src="/media/{{$t.Logo}}" alt="{{$r.Blog.Title}}"></a></div>{{end}}{{end}} -{{- else if eq $m.Kind "menu"}}{{if eq $m.Area "header"}}<div class="site-nav"><div class="wrap nav-inner">{{if $r.Data.layout.HeaderSearch}}{{template "searchform" $r}}{{end}}{{template "menulinks" $r}}</div></div> +{{- else if eq $m.Kind "menu"}}{{if eq $m.Area "header"}}<div class="site-nav{{if $b}} has-burger{{end}}"><div class="wrap nav-inner">{{if $b}}{{template "burger" $t}}{{end}}{{if $r.Data.layout.HeaderSearch}}{{template "searchform" $r}}{{end}}{{template "menulinks" $r}}</div></div> {{else}}<div class="mod site-nav mod-menu">{{template "modtitle" $m}}<div class="nav-inner">{{template "menulinks" $r}}</div></div>{{end}} {{- else if eq $m.Kind "recent"}}<div class="mod mod-recent">{{template "modtitle" $m}} <ul>{{range $i, $p := $r.Data.recent}}{{if lt $i $m.Count}}<li><a href="/{{$p.PageSlug}}/{{$p.Slug}}">{{$p.Title}}</a></li>{{end}}{{end}}</ul> diff --git a/internal/web/theme.go b/internal/web/theme.go index 031f8fd..a43603b 100644 --- a/internal/web/theme.go +++ b/internal/web/theme.go @@ -54,6 +54,10 @@ type Theme struct { NavSize string `json:"nav_size"` // normal | small | large NavGap string `json:"nav_gap"` // normal | tight | wide (space around each link) NavActive string `json:"nav_active"` // underline | bold | box | none: how the current page's link is marked + // NavFold folds the header menu into a button when its links do not fit + // the bar (see Layout.BurgerAt); NavFoldLabel is what the button shows. + NavFold bool `json:"nav_fold"` + NavFoldLabel string `json:"nav_fold_label"` // icon | text | both NavBg string `json:"nav_bg"` NavText string `json:"nav_text"` NavHover string `json:"nav_hover"` // "" = same as NavText @@ -118,7 +122,7 @@ func DefaultTheme() Theme { ContentStyle: "card", ContentPadding: "normal", Font: "sans", HeadingFont: "same", FontSize: "normal", HeaderImageHeight: "medium", HeaderAlign: "left", HeaderBg: "#2f3a4a", HeaderText: "#ffffff", TitleSize: "normal", HeaderRule: "none", - NavAlign: "left", NavBold: true, NavUnderline: "hover", NavSize: "normal", NavGap: "normal", NavActive: "underline", NavBg: "#ffffff", NavText: "#222222", + NavAlign: "left", NavBold: true, NavUnderline: "hover", NavSize: "normal", NavGap: "normal", NavActive: "underline", NavFoldLabel: "icon", NavBg: "#ffffff", NavText: "#222222", ShowDates: true, DateFormat: "long", PostsPerPage: postsPerPageChoices[0], ListStyle: "full", PostImage: "top", TitleRule: "none", PostNotices: true, FooterBg: "#2f3a4a", FooterColor: "#d0d5dc", FooterAlign: "center", LogoSize: "medium", @@ -213,6 +217,7 @@ func (t *Theme) normalize() { t.NavSize = pick(t.NavSize, "normal", "small", "large") t.NavGap = pick(t.NavGap, "normal", "tight", "wide") t.NavActive = pick(t.NavActive, "underline", "bold", "box", "none") + t.NavFoldLabel = pick(t.NavFoldLabel, "icon", "text", "both") t.NavBg = color(t.NavBg, d.NavBg) t.NavText = color(t.NavText, d.NavText) t.NavHover = optColor(t.NavHover) @@ -287,6 +292,8 @@ func ThemeFromForm(cur Theme, f url.Values) Theme { t.NavSize = get("nav_size") t.NavGap = get("nav_gap") t.NavActive = get("nav_active") + t.NavFold = on("nav_fold") + t.NavFoldLabel = get("nav_fold_label") t.NavBg = get("nav_bg") t.NavText = get("nav_text") t.NavHover = hover("nav_hover") @@ -591,7 +598,7 @@ a:hover {{"{"}}{{if .LinkHover}} color:{{.LinkHover}};{{end}}{{if eq .LinkUnderl .site-header a { color:{{.HeaderText}}; } .header-image img { max-height:{{.HeaderImageMaxHeight}}; } .site-nav { background:{{.NavBg}}; } -.site-nav a { color:{{.NavText}}; } +.site-nav a, .site-nav .nav-burger { color:{{.NavText}}; } {{- if .NavHover}} .site-nav a:hover { color:{{.NavHover}}; } {{- end}} diff --git a/internal/web/web_test.go b/internal/web/web_test.go index 45bdf22..12e30f2 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -126,7 +126,7 @@ func TestThemeNormalizeAndCSS(t *testing.T) { } // The menu links' weight, case and underline are body classes blog.css draws; the theme CSS only colours them. css = DefaultTheme().CSS() - if !strings.Contains(css, ".site-nav a { color:#222222; }") || strings.Contains(css, "font-weight") || strings.Contains(css, "text-transform") { + if !strings.Contains(css, ".site-nav a, .site-nav .nav-burger { color:#222222; }") || strings.Contains(css, "font-weight") || strings.Contains(css, "text-transform") { t.Errorf("nav css: %s", css) } if css := DefaultTheme().CSS(); strings.Contains(css, "@font-face") || strings.Contains(css, "h1, h2") { @@ -170,6 +170,80 @@ func TestNavStyles(t *testing.T) { } } +// The header menu folds into a button (theme nav_fold) carried by the title +// or logo block just above it, else by the menu bar; the checkbox that is the +// open/closed state comes first in the header so the CSS can reach both. +func TestHeaderFold(t *testing.T) { + hdr := func(kinds ...string) []store.Module { + var mods []store.Module + for _, k := range kinds { + mods = append(mods, store.Module{Area: "header", Kind: k}) + } + return mods + } + th := DefaultTheme() + if l := buildLayout(th, hdr("title", "menu")); l.Fold { + t.Errorf("fold is off by default: %+v", l) + } + th.NavFold = true + for _, c := range []struct { + kinds []string + logo bool + at int // -1: no fold + }{ + {[]string{"title", "menu"}, false, 0}, {[]string{"menu", "title"}, false, 0}, {[]string{"logo", "menu"}, false, 1}, {[]string{"logo", "menu"}, true, 0}, + {[]string{"search", "menu"}, false, 1}, {[]string{"title", "search", "menu"}, false, 2}, {[]string{"title"}, false, -1}, {nil, false, -1}, + } { + th.Logo = "" + if c.logo { + th.Logo = uuid.New().String() + } + l := buildLayout(th, hdr(c.kinds...)) + if l.Fold != (c.at >= 0) || (l.Fold && l.BurgerAt != c.at) { + t.Errorf("%v (logo %v): fold %v at %d, want %d", c.kinds, c.logo, l.Fold, l.BurgerAt, c.at) + } + } + th.HeaderOn = false + if l := buildLayout(th, hdr("title", "menu")); l.Fold { + t.Errorf("no header, nothing to fold: %+v", l) + } + th.HeaderOn = true + + tpl, err := newTemplates(false).get("en", "blog/page.html") + if err != nil { + t.Fatal(err) + } + render := func(th Theme, mods []store.Module) string { + var b strings.Builder + l := buildLayout(th, mods) + v := view{Blog: &store.Blog{Title: "Gizmo"}, Data: map[string]any{"theme": th, "layout": l, "menu": []store.MenuItem{{Label: "Home", URL: "/"}, {Label: "About", URL: "/about"}}}} + if err := tpl.ExecuteTemplate(&b, "siteheader", v); err != nil { + t.Fatal(err) + } + return b.String() + } + burger := `<label for="nav-toggle" class="nav-burger burger-icon"><span class="nb-icon" aria-hidden="true">☰</span><span class="nb-text">Menu</span></label>` + off := DefaultTheme() + out := render(off, hdr("title", "menu")) + if strings.Contains(out, "nav-toggle") || strings.Contains(out, "nav-burger") || strings.Contains(out, "nav-fold") || strings.Contains(out, "<script") { + t.Errorf("fold off: %s", out) + } + out = render(th, hdr("title", "menu")) + toggle, block, bar := strings.Index(out, `<input type="checkbox" id="nav-toggle" class="nav-toggle"`), strings.Index(out, `class="wrap header-text has-burger"`), strings.Index(out, `<div class="site-nav"><div class="wrap nav-inner">`) + if !strings.HasPrefix(out, `<div class="site-header nav-fold nav-auto">`) || toggle < 0 || block < toggle || bar < block || strings.Count(out, burger) != 1 || strings.Index(out, burger) > bar || !strings.Contains(out, "<script") { + t.Errorf("title host: %s", out) + } + out = render(th, hdr("menu", "title")) + if !strings.Contains(out, `<div class="site-nav has-burger"><div class="wrap nav-inner">`+burger+`<a href="/">`) || strings.Contains(out, "header-text has-burger") { + t.Errorf("menu first: %s", out) + } + th.HeaderAlign, th.NavFoldLabel, th.HeaderRule = "center", "both", "line" + out = render(th, hdr("title", "menu")) + if !strings.HasPrefix(out, `<div class="site-header nav-fold nav-auto header-center">`) || !strings.Contains(out, `class="wrap header-text ruled rule-line has-burger"`) || !strings.Contains(out, `</div><div class="header-rule"></div><label for="nav-toggle" class="nav-burger burger-both">`) { + t.Errorf("centred, ruled, both: %s", out) + } +} + // Announcements on post pages are on by default (the look blogs had before // the switch) and, like every checkbox, off when the box is unticked; stored // themes from before the option get the default too. |
