diff options
| -rw-r--r-- | AGENTS.md | 10 | ||||
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | internal/i18n/el.go | 3 | ||||
| -rw-r--r-- | internal/web/static/blog.css | 7 | ||||
| -rw-r--r-- | internal/web/templates/dashboard/design.html | 10 | ||||
| -rw-r--r-- | internal/web/templates/layouts/blog.html | 2 | ||||
| -rw-r--r-- | internal/web/theme.go | 47 | ||||
| -rw-r--r-- | internal/web/web_test.go | 41 |
8 files changed, 95 insertions, 29 deletions
@@ -213,9 +213,13 @@ internal/web/ server.go (host router, middleware, render helpers) `ruled rule-<kind>` 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). `TitleRule` (`none` | `line` | `dots` | `dots2` | `stripes`) is the header + 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 + (`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 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 @@ -35,8 +35,8 @@ Server-rendered HTML, no JavaScript required, works on old browsers and phones. set number of pixels, centred or at the left; 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, 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 + 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 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 e2523cb..bdec8a2 100644 --- a/internal/i18n/el.go +++ b/internal/i18n/el.go @@ -388,9 +388,8 @@ var el = map[string]string{ "Medium": "Μεσαίο", "Tall": "Ψηλό", "menu in the header": "μενού στην κεφαλίδα", - "Link style": "Στυλ συνδέσμων", + "Underline": "Υπογράμμιση", "Uppercase, spaced": "Κεφαλαία, αραιά", - "Plain, underlined": "Απλοί, υπογραμμισμένοι", "Show the date on posts": "Εμφάνιση ημερομηνίας στις δημοσιεύσεις", "Show tags with a # in front": "Εμφάνιση των ετικετών με # μπροστά", "Rule under titles": "Γραμμή κάτω από τους τίτλους", diff --git a/internal/web/static/blog.css b/internal/web/static/blog.css index 8cd969e..054532d 100644 --- a/internal/web/static/blog.css +++ b/internal/web/static/blog.css @@ -28,8 +28,11 @@ img { max-width: 100%; height: auto; } .tagline { margin: 0.3em 0 0; opacity: 0.85; } .site-nav { border-bottom: 1px solid rgba(0,0,0,0.1); } .nav-inner { padding: 0.3em 1em; } -.site-nav a { display: inline-block; padding: 0.5em 0.8em; text-decoration: none; font-weight: bold; } -.site-nav a.active, .site-nav a:hover { text-decoration: underline; } +.site-nav a { display: inline-block; padding: 0.5em 0.8em; text-decoration: none; } +/* 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; } .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 476e311..7b11e44 100644 --- a/internal/web/templates/dashboard/design.html +++ b/internal/web/templates/dashboard/design.html @@ -155,14 +155,16 @@ <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> </select></label> - <label>{{t "Link style"}}<br><select name="nav_style"> - <option value="normal"{{if eq $t.NavStyle "normal"}} selected{{end}}>{{t "Normal"}}</option> - <option value="uppercase"{{if eq $t.NavStyle "uppercase"}} selected{{end}}>{{t "Uppercase, spaced"}}</option> - <option value="plain"{{if eq $t.NavStyle "plain"}} selected{{end}}>{{t "Plain, underlined"}}</option> + <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 "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> + <label class="check"><input type="checkbox" name="nav_bold"{{if $t.NavBold}} checked{{end}}> {{t "Bold"}}</label> + <label class="check"><input type="checkbox" name="nav_upper"{{if $t.NavUpper}} checked{{end}}> {{t "Uppercase, spaced"}}</label> <div class="hoverpick"> <input type="checkbox" name="nav_hover_custom" id="nav-hover-custom"{{if $t.NavHover}} checked{{end}}> <label for="nav-hover-custom" class="check">{{t "Change colour when the mouse is over a link"}}</label> diff --git a/internal/web/templates/layouts/blog.html b/internal/web/templates/layouts/blog.html index 29147e7..7ee0b54 100644 --- a/internal/web/templates/layouts/blog.html +++ b/internal/web/templates/layouts/blog.html @@ -12,7 +12,7 @@ <style>{{css .Data.css}}</style> </head> {{$l := .Data.layout}} -<body class="{{if $l.HasLeft}}has-left {{end}}{{if $l.HasRight}}has-right {{end}}{{.Data.theme.TitleRuleClass}}"> +<body class="{{if $l.HasLeft}}has-left {{end}}{{if $l.HasRight}}has-right {{end}}{{.Data.theme.NavClass}}{{with .Data.theme.TitleRuleClass}} {{.}}{{end}}"> {{if or $l.Header .Data.theme.HeaderImage}}{{if .Data.theme.HeaderOn}}<div class="site-header"> {{if .Data.theme.HeaderImage}}<div class="header-image"><a href="/"><img src="/media/{{.Data.theme.HeaderImage}}" alt=""></a></div>{{end}} {{range $l.Header}}{{template "module" (dict "m" . "root" $)}}{{end}} diff --git a/internal/web/theme.go b/internal/web/theme.go index 0437f51..9eae856 100644 --- a/internal/web/theme.go +++ b/internal/web/theme.go @@ -43,10 +43,17 @@ type Theme struct { 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 | plain (regular weight, underlined) - NavBg string `json:"nav_bg"` - NavText string `json:"nav_text"` - NavHover string `json:"nav_hover"` // "" = same as NavText + // 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, + // turned into the switches by ParseTheme and never written again. + NavStyle string `json:"nav_style,omitempty"` + NavBold bool `json:"nav_bold"` + NavUpper bool `json:"nav_upper"` // uppercase, letter-spaced + NavUnderline string `json:"nav_underline"` // hover | never | always + NavBg string `json:"nav_bg"` + NavText string `json:"nav_text"` + NavHover string `json:"nav_hover"` // "" = same as NavText ShowDates bool `json:"show_dates"` DateFormat string `json:"date_format"` // long | short | iso (rendered by i18n.FormatDate) @@ -108,7 +115,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", NavStyle: "normal", NavBg: "#ffffff", NavText: "#222222", + NavAlign: "left", NavBold: true, NavUnderline: "hover", 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", @@ -122,6 +129,15 @@ func ParseTheme(raw json.RawMessage) Theme { if len(raw) > 0 { _ = json.Unmarshal(raw, &t) } + // Themes saved with the old link-style select: it becomes the switches + // it stood for, and is dropped from the JSON on the next save. + switch t.NavStyle { + case "uppercase": + t.NavUpper = true + case "plain": + t.NavBold, t.NavUnderline = false, "always" + } + t.NavStyle = "" t.normalize() return t } @@ -190,7 +206,7 @@ func (t *Theme) normalize() { 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", "plain") + t.NavUnderline = pick(t.NavUnderline, "hover", "never", "always") t.NavBg = color(t.NavBg, d.NavBg) t.NavText = color(t.NavText, d.NavText) t.NavHover = optColor(t.NavHover) @@ -259,7 +275,9 @@ func ThemeFromForm(cur Theme, f url.Values) Theme { t.TitleSize = get("title_size") t.HeaderRule = get("header_rule") t.NavAlign = get("nav_align") - t.NavStyle = get("nav_style") + t.NavBold = on("nav_bold") + t.NavUpper = on("nav_upper") + t.NavUnderline = get("nav_underline") t.NavBg = get("nav_bg") t.NavText = get("nav_text") t.NavHover = hover("nav_hover") @@ -504,6 +522,19 @@ 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. +func (t Theme) NavClass() string { + c := []string{"nav-ul-" + t.NavUnderline} + if t.NavBold { + c = append(c, "nav-bold") + } + if t.NavUpper { + c = append(c, "nav-upper") + } + return strings.Join(c, " ") +} + // TitleRuleClass is the body class blog.css draws the rule under page and // post titles from; the theme CSS only colours it. func (t Theme) TitleRuleClass() string { @@ -551,7 +582,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;{{else if eq .NavStyle "plain"}} font-weight:normal; text-decoration:underline;{{end}} } +.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 2f3bc4f..e12f9e0 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -101,17 +101,17 @@ func TestThemeNormalizeAndCSS(t *testing.T) { t.Errorf("old theme content defaults: %+v", th) } f := url.Values{"bg_color": {"#ABCDEF"}, "bg_image": {"not-a-uuid"}, "logo_size": {"large"}, - "link_hover": {"#ff0000"}, "nav_hover": {"#00ff00"}, "nav_hover_custom": {"on"}, "nav_style": {"uppercase"}, "nav_align": {"center"}, + "link_hover": {"#ff0000"}, "nav_hover": {"#00ff00"}, "nav_hover_custom": {"on"}, "nav_upper": {"on"}, "nav_align": {"center"}, "link_underline": {"hover"}, "heading_font": {"serif"}, "content_style": {"flat"}, "content_padding": {"roomy"}, "date_format": {"iso"}, "header_rule": {"stripes"}} th = ThemeFromForm(DefaultTheme(), f) 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" { + if th.LinkHover != "" || th.NavHover != "#00ff00" || th.ShowDates || th.DateFormat != "iso" || th.NavBold || !th.NavUpper { t.Errorf("hover checkbox / unticked checkboxes: %+v", th) } css := th.CSS() - for _, want := range []string{"background-color:#abcdef", "text-transform:uppercase", ".site-nav a:hover { color:#00ff00", "a:hover { text-decoration:underline", + 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"} { if !strings.Contains(css, want) { t.Errorf("css missing %q:\n%s", want, css) @@ -124,10 +124,10 @@ func TestThemeNormalizeAndCSS(t *testing.T) { if css := DefaultTheme().CSS(); strings.Contains(css, "padding:") || strings.Contains(css, "a:hover") { t.Errorf("default css should not set padding or hover: %s", css) } - // Plain menu links: regular weight and underlined, instead of blog.css's bold. - css = ThemeFromForm(DefaultTheme(), url.Values{"nav_style": {"plain"}}).CSS() - if !strings.Contains(css, ".site-nav a { color:#222222; font-weight:normal; text-decoration:underline; }") { - t.Errorf("plain nav css: %s", css) + // 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") { + t.Errorf("nav css: %s", css) } if css := DefaultTheme().CSS(); strings.Contains(css, "@font-face") || strings.Contains(css, "h1, h2") { t.Errorf("default css should declare no font faces or heading rule: %s", css) @@ -139,6 +139,33 @@ func TestThemeNormalizeAndCSS(t *testing.T) { } } +// The menu link styles are switches; the select they replaced is migrated +// 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") { + 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"}`} { + th = ParseTheme([]byte(raw)) + if !th.NavBold || th.NavUpper || th.NavUnderline != "hover" || th.NavClass() != "nav-ul-hover nav-bold" { + t.Errorf("%s: %+v %q", raw, th, th.NavClass()) + } + } + // New saves carry the switches: a stored false stays false, and unticked boxes are off. + 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" { + t.Errorf("from form: %+v %q", th, th.NavClass()) + } +} + // 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. |
