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/web/web_test.go | |
| 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/web/web_test.go')
| -rw-r--r-- | internal/web/web_test.go | 76 |
1 files changed, 75 insertions, 1 deletions
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. |
