From 69912c4ac04ada3286c09400ce523d5d3ab4cfb4 Mon Sep 17 00:00:00 2001 From: grm Date: Thu, 17 Sep 2026 20:15:14 +0300 Subject: Add a header rule beside the title or logo, and plain menu links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gizmolab's header is a logo with a striped rule filling the rest of the row, and a menu of plain underlined links under it; nothing in the Design tab could draw either. The rule (line, dots or stripes) is a theme option drawn in the header text colour beside the title and logo modules — on both sides when the header is centred — as table cells, so it is as tall as the brand and old browsers lay it out the same; it is hidden on phones so a long title can wrap. The menu's link style gains "plain, underlined" next to bold and uppercase. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A --- AGENTS.md | 9 +++++ README.md | 4 ++- internal/i18n/el.go | 6 ++++ internal/web/static/blog.css | 17 +++++++++ internal/web/templates/dashboard/design.html | 8 +++++ internal/web/templates/layouts/blog.html | 12 ++++--- internal/web/theme.go | 22 +++++++++--- internal/web/web_test.go | 54 +++++++++++++++++++++++++--- 8 files changed, 118 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b7bd557..84b6a7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -176,6 +176,15 @@ internal/web/ server.go (host router, middleware, render helpers) Summaries) and `PostImage` (where the post page shows the featured image: `top` | `bottom` | `list` = listings only). `TitleSize` is emitted inside the wide-screen media query so blog.css's phone rule keeps winning. + `HeaderRule` (`none` | `line` | `dots` | `stripes`) is a decoration beside + the title and logo modules in the header, filling the rest of the row (both + sides when `HeaderAlign` is `center`): `Theme.RuleClass` turns it into the + `ruled rule-` classes on the block, the `rule` template adds the + `.header-rule` cells and blog.css draws them — table cells (so the rule is + 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. `NavStyle` + `plain` is regular weight + underline for the menu links (blog.css makes + them bold). Optional colours (`LinkHover`, `NavHover`) are `""` = inherit and come from a colour input paired with a `_custom` checkbox, shown/hidden with CSS only (`.hoverpick > input:not(:checked) ~ …`). Image fields use the diff --git a/README.md b/README.md index 828ff20..295fe87 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,9 @@ Server-rendered HTML, no JavaScript required, works on old browsers and phones. case; several words match in order with anything in between), custom HTML, footer text, RSS link, site map. Column widths are percentages; on phones the columns stack under the posts; the header and footer can follow the main column's edges - for a centred, single-column look. The **menu** mixes blog pages and outside links. Posts per + 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, with + bold, uppercase or plain underlined links. 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 `` line, or after its first paragraphs). diff --git a/internal/i18n/el.go b/internal/i18n/el.go index 1fd6ceb..911eaf0 100644 --- a/internal/i18n/el.go +++ b/internal/i18n/el.go @@ -382,6 +382,7 @@ var el = map[string]string{ "menu in the header": "μενού στην κεφαλίδα", "Link style": "Στυλ συνδέσμων", "Uppercase, spaced": "Κεφαλαία, αραιά", + "Plain, underlined": "Απλοί, υπογραμμισμένοι", "Show the date on posts": "Εμφάνιση ημερομηνίας στις δημοσιεύσεις", "Date format": "Μορφή ημερομηνίας", "Logo & site icon": "Λογότυπο & εικονίδιο ιστολογίου", @@ -471,6 +472,11 @@ var el = map[string]string{ "Title size": "Μέγεθος τίτλου", "Banner image": "Εικόνα-πανό", "Banner height": "Ύψος πανό", + "Rule beside the title": "Γραμμή δίπλα στον τίτλο", + "Line": "Γραμμή", + "Dots": "Τελείες", + "Stripes": "Ρίγες", + "The rule is drawn in the text colour next to the title and logo modules, filling the rest of the row — on both sides when the header is centred. Wide screens only.": "Η γραμμή σχεδιάζεται στο χρώμα κειμένου δίπλα στα δομοστοιχεία τίτλου και λογότυπου, γεμίζοντας το υπόλοιπο της σειράς — και από τις δύο πλευρές όταν η κεφαλίδα είναι κεντραρισμένη. Μόνο σε πλατιές οθόνες.", "Shown across the top of the header whenever one is chosen.": "Εμφανίζεται κατά μήκος της κορυφής της κεφαλίδας όποτε έχει επιλεγεί κάποια.", "Stack the title, a logo, the menu and a search box in the order you like.": "Βάλτε τον τίτλο, ένα λογότυπο, το μενού και ένα πλαίσιο αναζήτησης με όποια σειρά θέλετε.", "A page or link filled in here is added when you save.": "Μια σελίδα ή ένας σύνδεσμος που συμπληρώνετε εδώ προστίθεται όταν αποθηκεύσετε.", diff --git a/internal/web/static/blog.css b/internal/web/static/blog.css index eabb314..10568fa 100644 --- a/internal/web/static/blog.css +++ b/internal/web/static/blog.css @@ -7,6 +7,22 @@ img { max-width: 100%; height: auto; } .header-text { padding: 1.4em 1em; } .header-logo { padding: 1em; } .header-logo img.logo { display: inline-block; width: auto; } +/* header rule: a line beside the title or logo (both sides when centred) filling + the rest of the row. Table cells, so the rule is as tall as the brand and old + browsers lay it out the same; the stripes are a gradient in currentColor (the + header text colour). Wide screens only: on a phone a long title must wrap. */ +.ruled { display: table; width: 100%; } +.ruled > .brand, .ruled > .header-rule { display: table-cell; vertical-align: middle; } +.ruled > .brand { white-space: nowrap; padding: 0 1em; } +.ruled > .brand:first-child { padding-left: 0; } +.ruled > .brand:last-child { padding-right: 0; } +.ruled > .brand a, .ruled > .brand img.logo { display: block; } /* no line box under the logo: the rule is exactly as tall as it */ +.ruled > .brand img.logo { max-width: none; } /* a percentage max-width is 0 inside an auto-width cell */ +.ruled > .header-rule { width: 100%; } +.ruled > .header-rule:first-child, .ruled > .header-rule:first-child ~ .header-rule { width: 50%; } /* centred: two rules share the remainder */ +.rule-line > .header-rule::before, .rule-dots > .header-rule::before { content: ""; display: block; border-top: 1px solid; } +.rule-dots > .header-rule::before { border-top: 3px dotted; } +.rule-stripes > .header-rule { background: repeating-linear-gradient(90deg, currentColor, currentColor 2px, transparent 2px, transparent 10px); } .site-title { margin: 0; font-size: 2em; line-height: 1.2; } .site-title a { text-decoration: none; } .tagline { margin: 0.3em 0 0; opacity: 0.85; } @@ -126,6 +142,7 @@ img { max-width: 100%; height: auto; } .content { padding: 1em; } .post-thumb { width: 40%; margin-left: 1em; } .site-title { font-size: 1.5em; } + .ruled > .header-rule { display: none; } .ruled > .brand { white-space: normal; } .cols { display: block; } .col-left, .col-right { padding: 0; margin-top: 1.5em; } .col-left { order: 0; } diff --git a/internal/web/templates/dashboard/design.html b/internal/web/templates/dashboard/design.html index c22a35e..5fd3fbe 100644 --- a/internal/web/templates/dashboard/design.html +++ b/internal/web/templates/dashboard/design.html @@ -101,7 +101,14 @@ + +

{{t "The rule is drawn in the text colour next to the title and logo modules, filling the rest of the row — on both sides when the header is centred. Wide screens only."}}

{{template "imagepick" (dict "name" "header_image" "current" $t.HeaderImage "sub" $sub "images" .Data.images "names" .Data.imageNames "label" (t "Banner image") "hint" (t "Shown across the top of the header whenever one is chosen."))}}
diff --git a/internal/web/templates/layouts/blog.html b/internal/web/templates/layouts/blog.html index 76475b9..ba9ea24 100644 --- a/internal/web/templates/layouts/blog.html +++ b/internal/web/templates/layouts/blog.html @@ -53,12 +53,12 @@ {{/* 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}} -{{- if eq $m.Kind "title"}}
+{{define "module"}}{{$m := .m}}{{$r := .root}}{{$t := $r.Data.theme}}{{$rc := $t.RuleClass}} +{{- if eq $m.Kind "title"}}
{{template "rule" (dict "before" true "rc" $rc "theme" $t)}}

{{$r.Blog.Title}}

{{if $r.Blog.Tagline}}

{{$r.Blog.Tagline}}

{{end}} -
-{{- else if eq $m.Kind "logo"}}{{if $t.Logo}}{{if eq $m.Area "header"}} +
{{template "rule" (dict "before" false "rc" $rc "theme" $t)}}
+{{- else if eq $m.Kind "logo"}}{{if $t.Logo}}{{if eq $m.Area "header"}}
{{template "rule" (dict "before" true "rc" $rc "theme" $t)}}
{{template "rule" (dict "before" false "rc" $rc "theme" $t)}}
{{else}}{{end}}{{end}} {{- else if eq $m.Kind "menu"}}{{if eq $m.Area "header"}} {{else}}{{end}} @@ -95,6 +95,10 @@ say "Search" over and over. .Data.q only exists on the search page. */}} {{define "searchform"}}
{{end}} +{{/* The header rule beside a title or logo block: after it, and before it + too when the header is centred. Nothing without a rule (.rc is empty). */}} +{{define "rule"}}{{if .rc}}{{if or (not .before) (eq .theme.HeaderAlign "center")}}
{{end}}{{end}}{{end}} + {{define "modtitle"}}{{if .Title}}

{{.Title}}

{{end}}{{end}} {{define "menulinks"}}{{range .Data.menu}}{{.Text}} diff --git a/internal/web/theme.go b/internal/web/theme.go index df2c613..c0c0729 100644 --- a/internal/web/theme.go +++ b/internal/web/theme.go @@ -34,10 +34,11 @@ type Theme struct { HeaderAlign string `json:"header_align"` // left | center HeaderBg string `json:"header_bg"` HeaderText string `json:"header_text"` - TitleSize string `json:"title_size"` // normal | small | large (the site title in the header) + 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) - NavStyle string `json:"nav_style"` // normal | uppercase + NavStyle string `json:"nav_style"` // normal | uppercase | plain (regular weight, underlined) NavBg string `json:"nav_bg"` NavText string `json:"nav_text"` NavHover string `json:"nav_hover"` // "" = same as NavText @@ -91,7 +92,7 @@ func DefaultTheme() Theme { ContentBg: "#ffffff", TextColor: "#222222", LinkColor: "#1a5fb4", LinkUnderline: "always", ContentStyle: "card", ContentPadding: "normal", Font: "sans", HeadingFont: "same", FontSize: "normal", - HeaderImageHeight: "medium", HeaderAlign: "left", HeaderBg: "#2f3a4a", HeaderText: "#ffffff", TitleSize: "normal", + HeaderImageHeight: "medium", HeaderAlign: "left", HeaderBg: "#2f3a4a", HeaderText: "#ffffff", TitleSize: "normal", HeaderRule: "none", NavAlign: "left", NavStyle: "normal", NavBg: "#ffffff", NavText: "#222222", ShowDates: true, DateFormat: "long", PostsPerPage: postsPerPageChoices[0], ListStyle: "full", PostImage: "top", FooterBg: "#2f3a4a", FooterColor: "#d0d5dc", FooterAlign: "center", @@ -170,8 +171,9 @@ func (t *Theme) normalize() { t.HeaderBg = color(t.HeaderBg, d.HeaderBg) 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.NavStyle = pick(t.NavStyle, "normal", "uppercase") + t.NavStyle = pick(t.NavStyle, "normal", "uppercase", "plain") t.NavBg = color(t.NavBg, d.NavBg) t.NavText = color(t.NavText, d.NavText) t.NavHover = optColor(t.NavHover) @@ -233,6 +235,7 @@ func ThemeFromForm(cur Theme, f url.Values) Theme { t.HeaderBg = get("header_bg") t.HeaderText = get("header_text") t.TitleSize = get("title_size") + t.HeaderRule = get("header_rule") t.NavAlign = get("nav_align") t.NavStyle = get("nav_style") t.NavBg = get("nav_bg") @@ -413,6 +416,15 @@ func (t Theme) TitleSizeCSS() string { return "" } +// RuleClass is what a title or logo block in the header carries when a rule +// is drawn beside it; blog.css does the drawing (normalize clamped the value). +func (t Theme) RuleClass() string { + if t.HeaderRule == "none" { + return "" + } + return "ruled rule-" + t.HeaderRule +} + func (t Theme) HeaderImageMaxHeight() string { switch t.HeaderImageHeight { case "short": @@ -442,7 +454,7 @@ a:hover {{"{"}}{{if .LinkHover}} color:{{.LinkHover}};{{end}}{{if eq .LinkUnderl .header-image img { max-height:{{.HeaderImageMaxHeight}}; } .site-nav { background:{{.NavBg}}; } .nav-inner { text-align:{{.NavAlign}}; } -.site-nav a { color:{{.NavText}};{{if eq .NavStyle "uppercase"}} text-transform:uppercase; letter-spacing:0.05em; font-size:0.9em;{{end}} } +.site-nav a { color:{{.NavText}};{{if eq .NavStyle "uppercase"}} text-transform:uppercase; letter-spacing:0.05em; font-size:0.9em;{{else if eq .NavStyle "plain"}} font-weight:normal; text-decoration:underline;{{end}} } {{- if .NavHover}} .site-nav a:hover { color:{{.NavHover}}; } {{- end}} diff --git a/internal/web/web_test.go b/internal/web/web_test.go index fcd1e41..914c991 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -82,8 +82,8 @@ func TestRootRoutePrecedence(t *testing.T) { } func TestThemeNormalizeAndCSS(t *testing.T) { - th := ParseTheme([]byte(`{"bg_color":"red;}body{display:none","font":"comic","bg_image":"../etc","logo":"../etc","link_hover":"blue","heading_font":"wingdings","favicon":"x","logo_size":"huge","left_width":5,"right_width":90}`)) - if th.BgColor != DefaultTheme().BgColor || th.Font != "sans" || th.BgImage != "" || th.Logo != "" || th.LinkHover != "" || th.HeadingFont != "same" || th.Favicon != "" || th.LogoSize != "medium" { + th := ParseTheme([]byte(`{"bg_color":"red;}body{display:none","font":"comic","bg_image":"../etc","logo":"../etc","link_hover":"blue","heading_font":"wingdings","favicon":"x","logo_size":"huge","left_width":5,"right_width":90,"header_rule":"neon\">