diff options
Diffstat (limited to 'internal/web/theme.go')
| -rw-r--r-- | internal/web/theme.go | 176 |
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}} |
