diff options
| author | grm <grm@eyesin.space> | 2026-09-18 19:18:30 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-18 19:18:30 +0300 |
| commit | 0a248d9a0e6ef6439fca25aa854968b6b5dac2ef (patch) | |
| tree | 54c770cfb8dd8c26d0fa128f584be6a8211d7181 | |
| parent | b88a7d22c8d79b30de3403c631dd2c5f67669537 (diff) | |
| download | blogspace-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
| -rw-r--r-- | AGENTS.md | 14 | ||||
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | internal/i18n/el.go | 8 | ||||
| -rw-r--r-- | internal/web/static/blog.css | 18 | ||||
| -rw-r--r-- | internal/web/templates/dashboard/design.html | 17 | ||||
| -rw-r--r-- | internal/web/theme.go | 25 | ||||
| -rw-r--r-- | internal/web/web_test.go | 18 |
7 files changed, 83 insertions, 21 deletions
@@ -215,11 +215,17 @@ internal/web/ server.go (host router, middleware, render helpers) as tall as the brand and old browsers agree) in `currentColor`, i.e. the header text colour; hidden on phones so a long title can wrap. The menu links' look is switches that combine — `NavBold`, `NavUpper`, `NavUnderline` - (`hover` | `never` | `always`) — turned into body classes by - `Theme.NavClass` (`nav-bold nav-upper nav-ul-<kind>`) that blog.css draws; - the theme CSS only colours the links. They replaced a `nav_style` select + (`hover` | `never` | `always`), `NavSize`, `NavGap`, `NavActive` (how the + current page's link is marked: `underline` | `bold` | `box` | `none`) and + `NavAlign` (`left` | `center` | `right` | `spread`) — turned into body + classes by `Theme.NavClass` (`nav-<align> nav-ul-<kind> nav-size-<s> + nav-gap-<g> nav-active-<a>`, plus `nav-bold`/`nav-upper`) that blog.css + draws; the theme CSS only colours the links. `spread` is justified text + with a full-width `::after` line, header only; `center`/`right` stay + 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 (`omitempty`, so the next save drops it). `TitleRule` (`none` | `line` | `dots` | `dots2` | `stripes`) is the header + onto the switches — `uppercase` was also the small size — (`omitempty`, so + 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 @@ -36,7 +36,9 @@ Server-rendered HTML, no JavaScript required, works on old browsers and phones. stack under the posts; the header and footer can follow the main column's edges for a centred, single-column look, and a rule (line, dots or stripes) can fill the 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, in any combination. Posts per + 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. 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 (a post's summary ends at a `<!--more-->` line, or after its first paragraphs). 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 |
