diff options
Diffstat (limited to 'internal/web')
| -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 |
5 files changed, 85 insertions, 22 deletions
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. |
