aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/theme.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/theme.go')
-rw-r--r--internal/web/theme.go176
1 files changed, 118 insertions, 58 deletions
diff --git a/internal/web/theme.go b/internal/web/theme.go
index 5ee2398..46067ba 100644
--- a/internal/web/theme.go
+++ b/internal/web/theme.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"net/url"
"regexp"
+ "strconv"
"strings"
"text/template"
"time"
@@ -23,50 +24,69 @@ type Theme struct {
LinkColor string `json:"link_color"`
LinkHover string `json:"link_hover"` // "" = same as LinkColor
LinkUnderline string `json:"link_underline"` // always | hover | never
- ContentWidth string `json:"content_width"` // narrow | medium | wide
+ ContentWidth string `json:"content_width"` // narrow | medium | wide (no side columns)
ContentStyle string `json:"content_style"` // card | flat
ContentPadding string `json:"content_padding"` // normal | compact | roomy
Font string `json:"font"` // sans | serif | mono
HeadingFont string `json:"heading_font"` // same | sans | serif | mono
FontSize string `json:"font_size"` // small | normal | large
- HeaderShowTitle bool `json:"header_show_title"`
HeaderImage string `json:"header_image"`
HeaderImageHeight string `json:"header_image_height"` // short | medium | tall
HeaderAlign string `json:"header_align"` // left | center
HeaderBg string `json:"header_bg"`
HeaderText string `json:"header_text"`
- NavPosition string `json:"nav_position"` // below-header | top-bar | left-sidebar
- NavAlign string `json:"nav_align"` // left | center | right (horizontal menus)
- NavSidebarWidth string `json:"nav_sidebar_width"` // narrow | normal | wide (sidebar only)
- NavStyle string `json:"nav_style"` // normal | uppercase
- NavBg string `json:"nav_bg"`
- NavText string `json:"nav_text"`
- NavHover string `json:"nav_hover"` // "" = same as NavText
- NavShowHome bool `json:"nav_show_home"`
+ NavAlign string `json:"nav_align"` // left | center | right (header menu)
+ NavStyle string `json:"nav_style"` // normal | uppercase
+ 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
- FooterText string `json:"footer_text"`
- FooterBg string `json:"footer_bg"`
- FooterColor string `json:"footer_color"`
- FooterShowRSS bool `json:"footer_show_rss"`
-
- Favicon string `json:"favicon"` // image uuid or "" for the Blogspace default
+ FooterBg string `json:"footer_bg"`
+ FooterColor string `json:"footer_color"`
+
+ Logo string `json:"logo"` // image uuid or ""
+ LogoSize string `json:"logo_size"` // small | medium | large
+ Favicon string `json:"favicon"` // image uuid or "" for the Blogspace default
+
+ // Layout: which areas are shown and how wide the side columns are. What
+ // each area contains lives in the modules table (store.Module).
+ HeaderOn bool `json:"header_on"`
+ LeftOn bool `json:"left_on"`
+ RightOn bool `json:"right_on"`
+ FooterOn bool `json:"footer_on"`
+ LeftWidth int `json:"left_width"` // percent of the page
+ RightWidth int `json:"right_width"` // percent of the page
+ // KeepColumns lays the side columns out even when empty or hidden, so
+ // the main content keeps the same width on every page.
+ KeepColumns bool `json:"keep_columns"`
}
+// Areas of the blog page, in page order. above/below sit inside the main column.
+var Areas = []string{"header", "left", "above", "below", "right", "footer"}
+
+const (
+ minColumnWidth = 15
+ maxColumnWidth = 40
+ maxColumnsSum = 70 // main keeps at least 30%
+)
+
func DefaultTheme() Theme {
return Theme{
BgColor: "#e9e6df", BgMode: "cover",
ContentBg: "#ffffff", TextColor: "#222222", LinkColor: "#1a5fb4", LinkUnderline: "always",
ContentWidth: "medium", ContentStyle: "card", ContentPadding: "normal",
Font: "sans", HeadingFont: "same", FontSize: "normal",
- HeaderShowTitle: true, HeaderImageHeight: "medium", HeaderAlign: "left", HeaderBg: "#2f3a4a", HeaderText: "#ffffff",
- NavPosition: "below-header", NavAlign: "left", NavSidebarWidth: "normal", NavStyle: "normal", NavBg: "#ffffff", NavText: "#222222", NavShowHome: true,
+ HeaderImageHeight: "medium", HeaderAlign: "left", HeaderBg: "#2f3a4a", HeaderText: "#ffffff",
+ NavAlign: "left", NavStyle: "normal", NavBg: "#ffffff", NavText: "#222222",
ShowDates: true, DateFormat: "long",
- FooterText: "", FooterBg: "#2f3a4a", FooterColor: "#d0d5dc", FooterShowRSS: true,
+ FooterBg: "#2f3a4a", FooterColor: "#d0d5dc",
+ LogoSize: "medium",
+ HeaderOn: true, LeftOn: true, RightOn: true, FooterOn: true, LeftWidth: 25, RightWidth: 25,
}
}
@@ -131,9 +151,7 @@ func (t *Theme) normalize() {
t.HeaderAlign = pick(t.HeaderAlign, "left", "center")
t.HeaderBg = color(t.HeaderBg, d.HeaderBg)
t.HeaderText = color(t.HeaderText, d.HeaderText)
- t.NavPosition = pick(t.NavPosition, "below-header", "top-bar", "left-sidebar")
t.NavAlign = pick(t.NavAlign, "left", "center", "right")
- t.NavSidebarWidth = pick(t.NavSidebarWidth, "normal", "narrow", "wide")
t.NavStyle = pick(t.NavStyle, "normal", "uppercase")
t.NavBg = color(t.NavBg, d.NavBg)
t.NavText = color(t.NavText, d.NavText)
@@ -141,10 +159,24 @@ func (t *Theme) normalize() {
t.DateFormat = pick(t.DateFormat, "long", "short", "iso")
t.FooterBg = color(t.FooterBg, d.FooterBg)
t.FooterColor = color(t.FooterColor, d.FooterColor)
- if len(t.FooterText) > 2000 {
- t.FooterText = t.FooterText[:2000]
- }
+ t.Logo = imageID(t.Logo)
+ t.LogoSize = pick(t.LogoSize, "medium", "small", "large")
t.Favicon = imageID(t.Favicon)
+ t.LeftWidth = clampInt(t.LeftWidth, minColumnWidth, maxColumnWidth)
+ t.RightWidth = clampInt(t.RightWidth, minColumnWidth, maxColumnWidth)
+ if t.LeftWidth+t.RightWidth > maxColumnsSum { // the right column gives way
+ t.RightWidth = maxColumnsSum - t.LeftWidth
+ }
+}
+
+func clampInt(v, lo, hi int) int {
+ if v < lo {
+ return lo
+ }
+ if v > hi {
+ return hi
+ }
+ return v
}
// ThemeFromForm reads the design form on top of the current theme.
@@ -172,32 +204,71 @@ func ThemeFromForm(cur Theme, f url.Values) Theme {
t.Font = get("font")
t.HeadingFont = get("heading_font")
t.FontSize = get("font_size")
- t.HeaderShowTitle = on("header_show_title")
t.HeaderImageHeight = get("header_image_height")
t.HeaderAlign = get("header_align")
t.HeaderBg = get("header_bg")
t.HeaderText = get("header_text")
- t.NavPosition = get("nav_position")
t.NavAlign = get("nav_align")
- t.NavSidebarWidth = get("nav_sidebar_width")
t.NavStyle = get("nav_style")
t.NavBg = get("nav_bg")
t.NavText = get("nav_text")
t.NavHover = hover("nav_hover")
- t.NavShowHome = on("nav_show_home")
t.ShowDates = on("show_dates")
t.DateFormat = get("date_format")
- t.FooterText = get("footer_text")
t.FooterBg = get("footer_bg")
t.FooterColor = get("footer_color")
- t.FooterShowRSS = on("footer_show_rss")
+ t.LogoSize = get("logo_size")
t.BgImage = pickImage(t.BgImage, get("bg_image"))
t.HeaderImage = pickImage(t.HeaderImage, get("header_image"))
t.Favicon = pickImage(t.Favicon, get("favicon"))
+ t.Logo = pickImage(t.Logo, get("logo"))
t.normalize()
return t
}
+// LayoutFromForm reads the columns form of the Layout page; the area
+// toggles have their own buttons (SetAreaOn).
+func LayoutFromForm(cur Theme, f url.Values) Theme {
+ t := cur
+ t.LeftWidth, _ = strconv.Atoi(strings.TrimSpace(f.Get("left_width")))
+ t.RightWidth, _ = strconv.Atoi(strings.TrimSpace(f.Get("right_width")))
+ t.KeepColumns = f.Get("keep_columns") == "on"
+ t.normalize()
+ return t
+}
+
+// AreaOn reports whether an area is switched on; above/below are part of the main column and always on.
+func (t Theme) AreaOn(area string) bool {
+ switch area {
+ case "header":
+ return t.HeaderOn
+ case "left":
+ return t.LeftOn
+ case "right":
+ return t.RightOn
+ case "footer":
+ return t.FooterOn
+ }
+ return true
+}
+
+// SetAreaOn flips an area's switch; unknown areas are ignored.
+func (t *Theme) SetAreaOn(area string, on bool) {
+ switch area {
+ case "header":
+ t.HeaderOn = on
+ case "left":
+ t.LeftOn = on
+ case "right":
+ t.RightOn = on
+ case "footer":
+ t.FooterOn = on
+ }
+}
+
+// MainWidth is what the main column gets when both side columns are shown.
+func (t Theme) MainWidth() int { return 100 - t.LeftWidth - t.RightWidth }
+
// pickImage applies an imagepick radio: "none" clears, an id selects, and an
// empty value (no radio in the form) keeps what was there.
func pickImage(cur, v string) string {
@@ -324,29 +395,15 @@ func (t Theme) ContentPaddingCSS() (desktop, phone string) {
func (t Theme) PaddingDesktop() string { d, _ := t.ContentPaddingCSS(); return d }
func (t Theme) PaddingPhone() string { _, p := t.ContentPaddingCSS(); return p }
-// SidebarWidthPx is the sidebar width when it differs from blog.css's 180px, else "".
-func (t Theme) SidebarWidthPx() string {
- if t.NavPosition != "left-sidebar" {
- return ""
- }
- switch t.NavSidebarWidth {
- case "narrow":
+// LogoMaxHeight sizes the logo image wherever the logo module appears.
+func (t Theme) LogoMaxHeight() string {
+ switch t.LogoSize {
+ case "small":
+ return "48px"
+ case "large":
return "140px"
- case "wide":
- return "240px"
}
- return ""
-}
-
-// SidebarContentMargin leaves the same 20px gutter blog.css uses.
-func (t Theme) SidebarContentMargin() string {
- switch t.NavSidebarWidth {
- case "narrow":
- return "160px"
- case "wide":
- return "260px"
- }
- return ""
+ return "80px"
}
func (t Theme) HeaderImageMaxHeight() string {
@@ -359,7 +416,7 @@ func (t Theme) HeaderImageMaxHeight() string {
return "320px"
}
-var themeCSS = template.Must(template.New("theme").Parse(`
+var themeCSS = template.Must(template.New("theme").Funcs(template.FuncMap{"sub": func(a, b int) int { return a - b }}).Parse(`
body { margin:0; background-color:{{.BgColor}}; color:{{.TextColor}}; font-family:{{.FontFamily}}; font-size:{{.FontSizePx}}; line-height:1.55;
{{- if .BgImage}} background-image:url(/media/{{.BgImage}});
{{- if eq .BgMode "cover"}} background-size:cover; background-position:center top; background-repeat:no-repeat;
@@ -383,12 +440,15 @@ a:hover {{"{"}}{{if .LinkHover}} color:{{.LinkHover}};{{end}}{{if eq .LinkUnderl
{{- if .NavHover}}
.site-nav a:hover { color:{{.NavHover}}; }
{{- end}}
-{{- with .SidebarWidthPx}}
-.nav-left-sidebar .body-wrap .site-nav, .nav-left-sidebar .body-wrap .sidebar-notices { width:{{.}}; }
-.nav-left-sidebar .body-wrap .content { margin-left:{{$.SidebarContentMargin}}; }
-@media (max-width: 700px) { .nav-left-sidebar .body-wrap .site-nav, .nav-left-sidebar .body-wrap .sidebar-notices { width:auto; } .nav-left-sidebar .body-wrap .content { margin-left:0; } }
-{{- end}}
-.content { background:{{.ContentBg}};{{with .PaddingDesktop}} padding:{{.}};{{end}}{{if eq .ContentStyle "flat"}} border-radius:0; box-shadow:none;{{end}} }
+.content, .mod { background:{{.ContentBg}};{{if eq .ContentStyle "flat"}} border-radius:0; box-shadow:none;{{end}} }
+.content {{"{"}}{{with .PaddingDesktop}} padding:{{.}};{{end}} }
+.logo { max-height:{{.LogoMaxHeight}}; }
+@media (min-width: 701px) {
+ .col-left { width:{{.LeftWidth}}%; } .col-right { width:{{.RightWidth}}%; }
+ body.has-left.has-right .col-main { width:{{.MainWidth}}%; }
+ body.has-left:not(.has-right) .col-main { width:{{sub 100 .LeftWidth}}%; }
+ body.has-right:not(.has-left) .col-main { width:{{sub 100 .RightWidth}}%; }
+}
{{- with .PaddingPhone}}
@media (max-width: 700px) { .content { padding:{{.}}; } }
{{- end}}