aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md28
-rw-r--r--README.md3
-rw-r--r--internal/i18n/el.go12
-rw-r--r--internal/web/layout.go18
-rw-r--r--internal/web/static/blog.css47
-rw-r--r--internal/web/templates/dashboard/design.html26
-rw-r--r--internal/web/templates/layouts/blog.html44
-rw-r--r--internal/web/theme.go11
-rw-r--r--internal/web/web_test.go76
9 files changed, 27 insertions, 238 deletions
diff --git a/AGENTS.md b/AGENTS.md
index baf174a..51c879d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -209,11 +209,9 @@ internal/web/ server.go (host router, middleware, render helpers)
editor's HTML preview, because the session cookie is `SameSite=Lax` and an
opaque origin would not send it for those images; safe only while no header
module kind is owner HTML (all four are template-escaped) and there is no
- `allow-scripts`. The frame runs no scripts, so the dashboard's script folds
- the menu inside it the way the blog's own script does (`fitFrame`), sizes
- the frame to the header, and re-runs on the frame's `change` (the fold
- button) and on the Wide/Phone toggle (the frame at 380px, so blog.css's
- phone rules apply).
+ `allow-scripts`. The dashboard's script sizes the frame to the header
+ (`fitFrame`, on load and on the Wide/Phone toggle — the frame at 380px, so
+ blog.css's phone rules apply).
- **Theme** (`web/theme.go`): struct stored as jsonb on `settings.theme`.
`ParseTheme` merges over `DefaultTheme()` and `normalize()` clamps every
value to an allowlist (hex colours, enum strings, uuid image ids, column
@@ -244,8 +242,7 @@ internal/web/ server.go (host router, middleware, render helpers)
unscoped (a column menu's block links centre their text, as before). They replaced a `nav_style` select
(`normal` | `uppercase` | `plain`), which `ParseTheme` still decodes and maps
onto the switches — `uppercase` was also the small size — (`omitempty`, so
- the next save drops it). `NavFold` folds the header menu into a button
- (`NavFoldLabel`: `icon` | `text` | `both`), see Layout. `TitleRule` (`none` | `line` | `dots` | `dots2` | `stripes`) is the header
+ the next save drops it). `TitleRule` (`none` | `line` | `dots` | `dots2` | `stripes`) is the header
rule's kinds, plus a double dotted row (`dots2`: the border and an
absolutely positioned `::after` with a second one 2px above), under page and post titles (`.page-title`, `.post-title`): a body
class from `Theme.TitleRuleClass` (`titles-<kind>`) that blog.css draws
@@ -316,22 +313,7 @@ internal/web/ server.go (host router, middleware, render helpers)
browsers without `min()` — wide screens only, backgrounds untouched);
below 700px `.cols` becomes block flow so the order is main → left →
right → footer. Everything is rendered by the `module` template in
- `layouts/blog.html`; the header by `siteheader` (the banner, then the
- modules). **Menu fold** (`theme.nav_fold`, `Layout.Fold`/`BurgerAt` from
- `buildLayout`): the header menu turns into a button when its links do not
- fit. The button (`burger` template, a `<label>`) is carried by the title or
- logo block just before the menu (`.has-burger`, the label absolutely
- positioned at the block's right — `header-center` pads the other side so
- the brand stays centred) or by the menu bar itself; a logo module without a
- logo renders nothing, so it cannot host it. A hidden, focusable checkbox
- (`#nav-toggle`) comes first in `.site-header` and is the open/closed state,
- so `:checked ~` reaches both blocks and no script is needed to open the
- stacked panel (links block-level, the search box on top). Folding itself:
- the inline script after the header (guarded; old browsers skip it) drops
- `.nav-auto` — the no-script fallback blog.css folds under 700px — and sets
- `.nav-folded` whenever a link's `offsetTop` differs from the first's, i.e.
- the row wrapped; re-run on `load` and `resize`. The `.nav-folded` and
- `@media … .nav-auto` rule blocks in blog.css must stay identical. New blogs get `insertDefaultModules` (title + menu
+ `layouts/blog.html`. New blogs get `insertDefaultModules` (title + menu
in the header, RSS in the footer) — keep it in step with the layout
migration's defaults.
- **Menu** (`menu_items` table, `store/menu.go`): the single ordered list
diff --git a/README.md b/README.md
index 120a9a6..0ca3d08 100644
--- a/README.md
+++ b/README.md
@@ -38,8 +38,7 @@ Server-rendered HTML, no JavaScript required, works on old browsers and phones.
row beside the title or logo. The **menu** mixes blog pages and outside links; its
links can be bold, uppercase, underlined always, on hover or never, small or large,
tight or wide apart, in any combination; the current page's link is underlined, bold,
- boxed or left alone, and the header menu sits left, centred, right or spread across the bar —
- or folds into a ☰ button (beside the title or logo above it) when its links do not fit.
+ boxed or left alone, and the header menu sits left, centred, right or spread across the bar.
A live preview of the header shows the menu as set, on a wide screen or a phone. Posts per
page, the date format, the site title size and the footer alignment live here too,
as does whether listings show whole posts or **summaries** with a "Read more" link
diff --git a/internal/i18n/el.go b/internal/i18n/el.go
index 00af43d..5d8a14e 100644
--- a/internal/i18n/el.go
+++ b/internal/i18n/el.go
@@ -397,14 +397,10 @@ var el = map[string]string{
"Underlined": "Υπογραμμισμένη",
"Boxed": "Σε πλαίσιο",
"Not marked": "Χωρίς σήμανση",
- "When the menu does not fit": "Όταν το μενού δεν χωράει",
- "Fold the header menu into a button when its links do not fit on one line": "Δίπλωμα του μενού της κεφαλίδας σε κουμπί όταν οι σύνδεσμοί του δεν χωρούν σε μία γραμμή",
- "Button": "Κουμπί",
- "Wide screen": "Πλατιά οθόνη",
- "Phone": "Κινητό",
- "Preview unavailable.": "Η προεπισκόπηση δεν είναι διαθέσιμη.",
- "Your header with the menu as set here; the phone view shows the folded menu. Nothing is saved until you press Save.": "Η κεφαλίδα σας με το μενού όπως ρυθμίζεται εδώ· η προβολή κινητού δείχνει το διπλωμένο μενού. Τίποτα δεν αποθηκεύεται μέχρι να πατήσετε Αποθήκευση.",
- "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 το βλέπουν στα κινητά.",
+ "Wide screen": "Πλατιά οθόνη",
+ "Phone": "Κινητό",
+ "Preview unavailable.": "Η προεπισκόπηση δεν είναι διαθέσιμη.",
+ "Your header with the menu as set here, on a wide screen or a phone. Nothing is saved until you press Save.": "Η κεφαλίδα σας με το μενού όπως ρυθμίζεται εδώ, σε πλατιά οθόνη ή κινητό. Τίποτα δεν αποθηκεύεται μέχρι να πατήσετε Αποθήκευση.",
"Uppercase, spaced": "Κεφαλαία, αραιά",
"Show the date on posts": "Εμφάνιση ημερομηνίας στις δημοσιεύσεις",
"Show tags with a # in front": "Εμφάνιση των ετικετών με # μπροστά",
diff --git a/internal/web/layout.go b/internal/web/layout.go
index d201758..91728ed 100644
--- a/internal/web/layout.go
+++ b/internal/web/layout.go
@@ -130,12 +130,6 @@ 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.
@@ -162,18 +156,6 @@ 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 b3b0312..7b91dbe 100644
--- a/internal/web/static/blog.css
+++ b/internal/web/static/blog.css
@@ -49,53 +49,6 @@ 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 039eabe..1af90c4 100644
--- a/internal/web/templates/dashboard/design.html
+++ b/internal/web/templates/dashboard/design.html
@@ -158,7 +158,7 @@
<div class="navpreview jsonly">
<p class="muted small">{{t "Preview"}}: <button type="button" class="mini" data-pv="wide" aria-pressed="true">{{t "Wide screen"}}</button> <button type="button" class="mini" data-pv="phone" aria-pressed="false">{{t "Phone"}}</button> <span class="pvstatus"></span></p>
<iframe class="navpreview-frame" sandbox="allow-same-origin" scrolling="no" title="{{t "Preview"}}"></iframe>
- <p class="muted small">{{t "Your header with the menu as set here; the phone view shows the folded menu. Nothing is saved until you press Save."}}</p>
+ <p class="muted small">{{t "Your header with the menu as set here, on a wide screen or a phone. Nothing is saved until you press Save."}}</p>
</div>
<div class="row">
<label>{{t "Alignment"}} <span class="muted">({{t "menu in the header"}})</span><br><select name="nav_align">
@@ -198,17 +198,6 @@
<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}}>&#9776;</option>
- <option value="text"{{if eq $t.NavFoldLabel "text"}} selected{{end}}>{{t "Menu"}}</option>
- <option value="both"{{if eq $t.NavFoldLabel "both"}} selected{{end}}>&#9776; {{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">
@@ -380,18 +369,11 @@
.then(function () { pvBusy = false; if (pvAgain) { pvAgain = false; preview(); } });
}
function schedule() { clearTimeout(pvTimer); pvTimer = setTimeout(preview, 300); }
- function fitFrame() { // the frame runs no scripts, so fold the menu the way the blog's own script does, and size the frame to the header
- try {
- var d = frame.contentDocument, h = d.querySelector(".site-header.nav-fold"), links = h ? h.querySelectorAll(".site-nav .nav-inner a") : [];
- if (links.length) {
- h.classList.remove("nav-auto"); h.classList.remove("nav-folded");
- for (var i = 1; i < links.length; i++) if (links[i].offsetTop !== links[0].offsetTop) { h.classList.add("nav-folded"); break; }
- }
- frame.style.height = d.body.scrollHeight + "px";
- } catch (e) {}
+ function fitFrame() { // the frame is as tall as the header
+ try { frame.style.height = frame.contentDocument.body.scrollHeight + "px"; } catch (e) {}
}
if (frame) {
- frame.addEventListener("load", function () { fitFrame(); try { frame.contentDocument.addEventListener("change", fitFrame); } catch (e) {} }); // change: the fold button opened or closed the panel
+ frame.addEventListener("load", fitFrame);
qa("[data-pv]").forEach(function (b) { b.addEventListener("click", function () {
frame.classList.toggle("phone", b.getAttribute("data-pv") === "phone");
qa("[data-pv]").forEach(function (o) { o.setAttribute("aria-pressed", String(o === b)); });
diff --git a/internal/web/templates/layouts/blog.html b/internal/web/templates/layouts/blog.html
index 71a62b0..99bb13a 100644
--- a/internal/web/templates/layouts/blog.html
+++ b/internal/web/templates/layouts/blog.html
@@ -40,36 +40,12 @@
</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}}
+{{/* The header: the banner, then its modules in order. On its own so the
+ design page's preview can render it alone. */}}
+{{define "siteheader"}}{{$t := .Data.theme}}<div class="site-header">
{{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">&#9776;</span><span class="nb-text">{{t "Menu"}}</span></label>{{end}}
+ {{range .Data.layout.Header}}{{template "module" (dict "m" . "root" $)}}{{end}}
+</div>{{end}}
{{define "notices"}}{{if .}}<div class="notices">
{{range .}}<aside class="notice notice-{{.Style}}{{if .Title}} headed{{end}}{{if .Width}} w-{{.Width}}{{end}}">
@@ -81,14 +57,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}}{{$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">
+{{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">
<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)}}{{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="{{$r.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>
+</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="{{$r.Media}}/{{$t.Logo}}" alt="{{$r.Blog.Title}}"></a></div>{{template "rule" (dict "before" false "rc" $rc "theme" $t)}}</div>
{{else}}<div class="mod mod-logo"><a href="/"><img class="logo" src="{{$r.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{{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 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}}<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 ee3ef11..0b24a87 100644
--- a/internal/web/theme.go
+++ b/internal/web/theme.go
@@ -54,10 +54,6 @@ 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
@@ -122,7 +118,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", NavFoldLabel: "icon", NavBg: "#ffffff", NavText: "#222222",
+ NavAlign: "left", NavBold: true, NavUnderline: "hover", NavSize: "normal", NavGap: "normal", NavActive: "underline", 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",
@@ -217,7 +213,6 @@ 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)
@@ -292,8 +287,6 @@ 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")
@@ -598,7 +591,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, .site-nav .nav-burger { color:{{.NavText}}; }
+.site-nav a { 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 e9e38fb..fc255b1 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, .site-nav .nav-burger { color:#222222; }") || strings.Contains(css, "font-weight") || strings.Contains(css, "text-transform") {
+ if !strings.Contains(css, ".site-nav a { 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,80 +170,6 @@ 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"}, Media: "/media", 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">&#9776;</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)
- }
-}
-
// The design page's preview is the header alone as a page of its own, with
// the blog's files linked through the dashboard's path for them.
func TestDesignPreview(t *testing.T) {