aboutsummaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-18 19:18:30 +0300
committergrm <grm@eyesin.space>2026-09-18 19:18:30 +0300
commit0a248d9a0e6ef6439fca25aa854968b6b5dac2ef (patch)
tree54c770cfb8dd8c26d0fa128f584be6a8211d7181 /internal
parentb88a7d22c8d79b30de3403c631dd2c5f67669537 (diff)
downloadblogspace-0a248d9a0e6ef6439fca25aa854968b6b5dac2ef.tar.gz
blogspace-0a248d9a0e6ef6439fca25aa854968b6b5dac2ef.tar.bz2
blogspace-0a248d9a0e6ef6439fca25aa854968b6b5dac2ef.zip
Add menu link size, spacing, a current-page mark and a spread alignment
Size and spacing selects, a choice of how the current page's link is marked (underlined as before, bold, boxed or not at all) and a fourth alignment that spreads the header's links across the bar. Alignment joins the other menu classes on the body, so the theme CSS no longer sets it; spread is justified text with a full-width last line, which old browsers lay out too, and stays out of the column menus. 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.go8
-rw-r--r--internal/web/static/blog.css18
-rw-r--r--internal/web/templates/dashboard/design.html17
-rw-r--r--internal/web/theme.go25
-rw-r--r--internal/web/web_test.go18
5 files changed, 70 insertions, 16 deletions
diff --git a/internal/i18n/el.go b/internal/i18n/el.go
index bdec8a2..2afef53 100644
--- a/internal/i18n/el.go
+++ b/internal/i18n/el.go
@@ -389,6 +389,14 @@ var el = map[string]string{
"Tall": "Ψηλό",
"menu in the header": "μενού στην κεφαλίδα",
"Underline": "Υπογράμμιση",
+ "Spread out": "Απλωμένοι",
+ "Spacing": "Απόσταση",
+ "Tight": "Σφιχτή",
+ "Wide": "Πλατιά",
+ "Current page": "Τρέχουσα σελίδα",
+ "Underlined": "Υπογραμμισμένη",
+ "Boxed": "Σε πλαίσιο",
+ "Not marked": "Χωρίς σήμανση",
"Uppercase, spaced": "Κεφαλαία, αραιά",
"Show the date on posts": "Εμφάνιση ημερομηνίας στις δημοσιεύσεις",
"Show tags with a # in front": "Εμφάνιση των ετικετών με # μπροστά",
diff --git a/internal/web/static/blog.css b/internal/web/static/blog.css
index 054532d..7b91dbe 100644
--- a/internal/web/static/blog.css
+++ b/internal/web/static/blog.css
@@ -32,7 +32,23 @@ img { max-width: 100%; height: auto; }
/* menu link styles (body.nav-*, from the theme's switches; colours are the theme CSS's) */
.nav-bold .site-nav a { font-weight: bold; }
.nav-upper .site-nav a { text-transform: uppercase; letter-spacing: 0.05em; }
-.nav-ul-always .site-nav a, .nav-ul-hover .site-nav a:hover, .site-nav a.active { text-decoration: underline; }
+.nav-ul-always .site-nav a, .nav-ul-hover .site-nav a:hover { text-decoration: underline; }
+.nav-size-small .site-nav a { font-size: 0.9em; }
+.nav-size-large .site-nav a { font-size: 1.15em; }
+.nav-gap-tight .site-nav a { padding-left: 0.4em; padding-right: 0.4em; }
+.nav-gap-wide .site-nav a { padding-left: 1.4em; padding-right: 1.4em; }
+.nav-active-underline .site-nav a.active { text-decoration: underline; }
+.nav-active-bold .site-nav a.active { font-weight: bold; }
+.nav-active-box .site-nav a.active { box-shadow: inset 0 0 0 1px currentColor; border-radius: 3px; }
+.nav-center .nav-inner { text-align: center; }
+.nav-right .nav-inner { text-align: right; }
+/* spread: the header's links share the bar evenly — justified text with a
+ full-width last line, which old browsers lay out too; line-height 0 keeps
+ that forced line from adding height, the links carry their own. A floated
+ search box just shortens the line. */
+.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%; }
.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 7b11e44..ec4f70d 100644
--- a/internal/web/templates/dashboard/design.html
+++ b/internal/web/templates/dashboard/design.html
@@ -154,12 +154,29 @@
<option value="left"{{if eq $t.NavAlign "left"}} selected{{end}}>{{t "Left"}}</option>
<option value="center"{{if eq $t.NavAlign "center"}} selected{{end}}>{{t "Centred"}}</option>
<option value="right"{{if eq $t.NavAlign "right"}} selected{{end}}>{{t "Right"}}</option>
+ <option value="spread"{{if eq $t.NavAlign "spread"}} selected{{end}}>{{t "Spread out"}}</option>
+ </select></label>
+ <label>{{t "Size"}}<br><select name="nav_size">
+ <option value="small"{{if eq $t.NavSize "small"}} selected{{end}}>{{t "Small"}}</option>
+ <option value="normal"{{if eq $t.NavSize "normal"}} selected{{end}}>{{t "Normal"}}</option>
+ <option value="large"{{if eq $t.NavSize "large"}} selected{{end}}>{{t "Large"}}</option>
+ </select></label>
+ <label>{{t "Spacing"}}<br><select name="nav_gap">
+ <option value="tight"{{if eq $t.NavGap "tight"}} selected{{end}}>{{t "Tight"}}</option>
+ <option value="normal"{{if eq $t.NavGap "normal"}} selected{{end}}>{{t "Normal"}}</option>
+ <option value="wide"{{if eq $t.NavGap "wide"}} selected{{end}}>{{t "Wide"}}</option>
</select></label>
<label>{{t "Underline"}}<br><select name="nav_underline">
<option value="never"{{if eq $t.NavUnderline "never"}} selected{{end}}>{{t "Never"}}</option>
<option value="hover"{{if eq $t.NavUnderline "hover"}} selected{{end}}>{{t "Only on hover"}}</option>
<option value="always"{{if eq $t.NavUnderline "always"}} selected{{end}}>{{t "Always"}}</option>
</select></label>
+ <label>{{t "Current page"}}<br><select name="nav_active">
+ <option value="underline"{{if eq $t.NavActive "underline"}} selected{{end}}>{{t "Underlined"}}</option>
+ <option value="bold"{{if eq $t.NavActive "bold"}} selected{{end}}>{{t "Bold"}}</option>
+ <option value="box"{{if eq $t.NavActive "box"}} selected{{end}}>{{t "Boxed"}}</option>
+ <option value="none"{{if eq $t.NavActive "none"}} selected{{end}}>{{t "Not marked"}}</option>
+ </select></label>
<label>{{t "Background"}}<br><input type="color" name="nav_bg" value="{{$t.NavBg}}"></label>
<label>{{t "Text"}}<br><input type="color" name="nav_text" value="{{$t.NavText}}"></label>
</div>
diff --git a/internal/web/theme.go b/internal/web/theme.go
index 9eae856..031f8fd 100644
--- a/internal/web/theme.go
+++ b/internal/web/theme.go
@@ -42,7 +42,7 @@ type Theme struct {
TitleSize string `json:"title_size"` // normal | small | large (the site title in the header)
HeaderRule string `json:"header_rule"` // none | line | dots | stripes, drawn beside the title/logo modules in the header
- NavAlign string `json:"nav_align"` // left | center | right (header menu)
+ NavAlign string `json:"nav_align"` // left | center | right | spread (header menu)
// The link styles are switches that combine; blog.css draws them from
// the body classes NavClass makes. NavStyle is the select they replaced
// (normal | uppercase | plain): still decoded from themes saved before,
@@ -51,6 +51,9 @@ type Theme struct {
NavBold bool `json:"nav_bold"`
NavUpper bool `json:"nav_upper"` // uppercase, letter-spaced
NavUnderline string `json:"nav_underline"` // hover | never | always
+ 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
NavBg string `json:"nav_bg"`
NavText string `json:"nav_text"`
NavHover string `json:"nav_hover"` // "" = same as NavText
@@ -115,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", 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",
@@ -133,7 +136,7 @@ func ParseTheme(raw json.RawMessage) Theme {
// it stood for, and is dropped from the JSON on the next save.
switch t.NavStyle {
case "uppercase":
- t.NavUpper = true
+ t.NavUpper, t.NavSize = true, "small" // the smaller size was part of that look
case "plain":
t.NavBold, t.NavUnderline = false, "always"
}
@@ -205,8 +208,11 @@ func (t *Theme) normalize() {
t.HeaderText = color(t.HeaderText, d.HeaderText)
t.TitleSize = pick(t.TitleSize, "normal", "small", "large")
t.HeaderRule = pick(t.HeaderRule, "none", "line", "dots", "stripes")
- t.NavAlign = pick(t.NavAlign, "left", "center", "right")
+ t.NavAlign = pick(t.NavAlign, "left", "center", "right", "spread")
t.NavUnderline = pick(t.NavUnderline, "hover", "never", "always")
+ 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.NavBg = color(t.NavBg, d.NavBg)
t.NavText = color(t.NavText, d.NavText)
t.NavHover = optColor(t.NavHover)
@@ -278,6 +284,9 @@ func ThemeFromForm(cur Theme, f url.Values) Theme {
t.NavBold = on("nav_bold")
t.NavUpper = on("nav_upper")
t.NavUnderline = get("nav_underline")
+ t.NavSize = get("nav_size")
+ t.NavGap = get("nav_gap")
+ t.NavActive = get("nav_active")
t.NavBg = get("nav_bg")
t.NavText = get("nav_text")
t.NavHover = hover("nav_hover")
@@ -522,10 +531,11 @@ func (t Theme) RuleClass() string {
return "ruled rule-" + t.HeaderRule
}
-// NavClass is the body classes blog.css draws the menu links from (weight,
-// case, underline); the theme CSS only colours them.
+// NavClass is the body classes blog.css draws the menu links from (alignment,
+// underline, size, spacing, the current page's mark, weight, case); the theme
+// CSS only colours them.
func (t Theme) NavClass() string {
- c := []string{"nav-ul-" + t.NavUnderline}
+ c := []string{"nav-" + t.NavAlign, "nav-ul-" + t.NavUnderline, "nav-size-" + t.NavSize, "nav-gap-" + t.NavGap, "nav-active-" + t.NavActive}
if t.NavBold {
c = append(c, "nav-bold")
}
@@ -581,7 +591,6 @@ 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}}; }
-.nav-inner { text-align:{{.NavAlign}}; }
.site-nav a { color:{{.NavText}}; }
{{- if .NavHover}}
.site-nav a:hover { color:{{.NavHover}}; }
diff --git a/internal/web/web_test.go b/internal/web/web_test.go
index e12f9e0..45bdf22 100644
--- a/internal/web/web_test.go
+++ b/internal/web/web_test.go
@@ -107,12 +107,12 @@ func TestThemeNormalizeAndCSS(t *testing.T) {
if th.BgColor != "#abcdef" || th.BgImage != "" || th.LogoSize != "large" || th.RuleClass() != "ruled rule-stripes" {
t.Errorf("from form: %+v", th)
}
- if th.LinkHover != "" || th.NavHover != "#00ff00" || th.ShowDates || th.DateFormat != "iso" || th.NavBold || !th.NavUpper {
+ if th.LinkHover != "" || th.NavHover != "#00ff00" || th.ShowDates || th.DateFormat != "iso" || th.NavBold || !th.NavUpper || th.NavAlign != "center" {
t.Errorf("hover checkbox / unticked checkboxes: %+v", th)
}
css := th.CSS()
for _, want := range []string{"background-color:#abcdef", ".site-nav a:hover { color:#00ff00", "a:hover { text-decoration:underline",
- "h1, h2, h3, h4, h5, h6 { font-family:Georgia", "text-align:center", "border-radius:0", "padding:2.2em 3em", "@media (max-width: 700px)", ".logo { max-height:140px"} {
+ "h1, h2, h3, h4, h5, h6 { font-family:Georgia", "border-radius:0", "padding:2.2em 3em", "@media (max-width: 700px)", ".logo { max-height:140px"} {
if !strings.Contains(css, want) {
t.Errorf("css missing %q:\n%s", want, css)
}
@@ -143,16 +143,16 @@ func TestThemeNormalizeAndCSS(t *testing.T) {
// from stored themes (bold + hover underline is what blog.css always drew).
func TestNavStyles(t *testing.T) {
th := ParseTheme([]byte(`{"nav_style":"uppercase"}`))
- if !th.NavUpper || !th.NavBold || th.NavUnderline != "hover" || th.NavStyle != "" || strings.Contains(string(th.JSON()), "nav_style") {
+ if !th.NavUpper || th.NavSize != "small" || !th.NavBold || th.NavUnderline != "hover" || th.NavStyle != "" || strings.Contains(string(th.JSON()), "nav_style") {
t.Errorf("uppercase migration: %+v %s", th, th.JSON())
}
th = ParseTheme([]byte(`{"nav_style":"plain"}`))
if th.NavBold || th.NavUpper || th.NavUnderline != "always" {
t.Errorf("plain migration: %+v", th)
}
- for _, raw := range []string{`{}`, `{"nav_style":"normal"}`, `{"nav_underline":"wavy"}`} {
+ for _, raw := range []string{`{}`, `{"nav_style":"normal"}`, `{"nav_underline":"wavy","nav_size":"huge","nav_gap":"x","nav_active":"blink","nav_align":"justify"}`} {
th = ParseTheme([]byte(raw))
- if !th.NavBold || th.NavUpper || th.NavUnderline != "hover" || th.NavClass() != "nav-ul-hover nav-bold" {
+ if !th.NavBold || th.NavUpper || th.NavUnderline != "hover" || th.NavClass() != "nav-left nav-ul-hover nav-size-normal nav-gap-normal nav-active-underline nav-bold" {
t.Errorf("%s: %+v %q", raw, th, th.NavClass())
}
}
@@ -160,10 +160,14 @@ func TestNavStyles(t *testing.T) {
if th = ParseTheme([]byte(`{"nav_bold":false,"nav_upper":true,"nav_underline":"never"}`)); th.NavBold || !th.NavUpper || th.NavUnderline != "never" {
t.Errorf("stored switches: %+v", th)
}
- th = ThemeFromForm(DefaultTheme(), url.Values{"nav_underline": {"always"}})
- if th.NavBold || th.NavUpper || th.NavClass() != "nav-ul-always" {
+ th = ThemeFromForm(DefaultTheme(), url.Values{"nav_underline": {"always"}, "nav_align": {"spread"}, "nav_size": {"large"}, "nav_gap": {"wide"}, "nav_active": {"box"}})
+ if th.NavBold || th.NavUpper || th.NavClass() != "nav-spread nav-ul-always nav-size-large nav-gap-wide nav-active-box" {
t.Errorf("from form: %+v %q", th, th.NavClass())
}
+ // Alignment is a class now; the theme CSS says nothing about it.
+ if css := th.CSS(); strings.Contains(css, "nav-inner") {
+ t.Errorf("nav align in css: %s", css)
+ }
}
// Announcements on post pages are on by default (the look blogs had before