aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web')
-rw-r--r--internal/web/templates/dashboard/design.html8
-rw-r--r--internal/web/theme.go48
-rw-r--r--internal/web/web_test.go32
3 files changed, 83 insertions, 5 deletions
diff --git a/internal/web/templates/dashboard/design.html b/internal/web/templates/dashboard/design.html
index 49f4699..31913f6 100644
--- a/internal/web/templates/dashboard/design.html
+++ b/internal/web/templates/dashboard/design.html
@@ -238,6 +238,14 @@
<label>{{t "Right column width"}}<br><input type="number" name="right_width" value="{{$t.RightWidth}}" min="15" max="40" step="5"> %</label>
</div>
<p class="muted small">{{t "Between 15% and 40% each; the main content takes the rest. A column only appears when it has at least one module; without columns the posts take the whole width. On phones the columns stack: main content first, then the left and right columns, then the footer."}}</p>
+ <div class="row">
+ <label>{{t "Page width"}}<br><input type="number" name="page_width" value="{{if $t.PageWidth}}{{$t.PageWidth}}{{end}}" min="400" max="3000" step="10" placeholder="{{t "the screen"}}"> px</label>
+ <label>{{t "Page position"}}<br><select name="page_align">
+ <option value="center"{{if eq $t.PageAlign "center"}} selected{{end}}>{{t "Centred"}}</option>
+ <option value="left"{{if eq $t.PageAlign "left"}} selected{{end}}>{{t "Left"}}</option>
+ </select></label>
+ </div>
+ <p class="muted small">{{t "Leave the width blank for the whole screen. With one, the columns share that many pixels at most — and so do the header and footer when they follow the main column (below); their backgrounds always span the screen."}}</p>
<label class="check"><input type="checkbox" name="keep_columns"{{if $t.KeepColumns}} checked{{end}}> {{t "Always keep the columns' space, even when a column is empty or hidden"}} <span class="muted">({{t "the main content then keeps its width and stays centred"}})</span></label>
<label class="check"><input type="checkbox" name="align_to_main"{{if $t.AlignToMain}} checked{{end}}> {{t "Header and footer follow the main column"}} <span class="muted">({{t "their text lines up with the main content instead of the page edges; the backgrounds stay full width"}})</span></label>
<h3>{{t "Left column"}}</h3>
diff --git a/internal/web/theme.go b/internal/web/theme.go
index 66ac7ad..d686c4e 100644
--- a/internal/web/theme.go
+++ b/internal/web/theme.go
@@ -79,6 +79,11 @@ type Theme struct {
// column's edges (the backgrounds stay full width), so a blog with
// KeepColumns and empty columns reads as one centred column.
AlignToMain bool `json:"align_to_main"`
+ // PageWidth caps the columns' box at so many pixels (0 = the screen),
+ // centred or at the left per PageAlign; the header's and footer's
+ // content follows it when AlignToMain is on, their backgrounds never.
+ PageWidth int `json:"page_width"`
+ PageAlign string `json:"page_align"` // center | left
}
// Areas of the blog page, in page order. above/below sit inside the main column.
@@ -88,6 +93,8 @@ const (
minColumnWidth = 15
maxColumnWidth = 40
maxColumnsSum = 70 // main keeps at least 30%
+ minPageWidth = 400
+ maxPageWidth = 3000
)
// postsPerPageChoices are the listing sizes the design form offers; the first is the default.
@@ -104,7 +111,7 @@ func DefaultTheme() Theme {
ShowDates: true, DateFormat: "long", PostsPerPage: postsPerPageChoices[0], ListStyle: "full", PostImage: "top", TitleRule: "none",
FooterBg: "#2f3a4a", FooterColor: "#d0d5dc", FooterAlign: "center",
LogoSize: "medium",
- HeaderOn: true, LeftOn: true, RightOn: true, FooterOn: true, LeftWidth: 25, RightWidth: 25,
+ HeaderOn: true, LeftOn: true, RightOn: true, FooterOn: true, LeftWidth: 25, RightWidth: 25, PageAlign: "center",
}
}
@@ -202,6 +209,10 @@ func (t *Theme) normalize() {
if t.LeftWidth+t.RightWidth > maxColumnsSum { // the right column gives way
t.RightWidth = maxColumnsSum - t.LeftWidth
}
+ if t.PageWidth != 0 {
+ t.PageWidth = clampInt(t.PageWidth, minPageWidth, maxPageWidth)
+ }
+ t.PageAlign = pick(t.PageAlign, "center", "left")
}
func clampInt(v, lo, hi int) int {
@@ -272,6 +283,8 @@ func ThemeFromForm(cur Theme, f url.Values) Theme {
t.LeftWidth, t.RightWidth = num("left_width"), num("right_width")
t.KeepColumns = on("keep_columns")
t.AlignToMain = on("align_to_main")
+ t.PageWidth = num("page_width")
+ t.PageAlign = get("page_align")
t.normalize()
return t
}
@@ -294,6 +307,29 @@ func (t Theme) AreaOn(area string) bool {
// MainWidth is what the main column gets when both side columns are shown.
func (t Theme) MainWidth() int { return 100 - t.LeftWidth - t.RightWidth }
+// PageMargin centres the capped page box or leaves it at the left.
+func (t Theme) PageMargin() string {
+ if t.PageAlign == "left" {
+ return "0"
+ }
+ return "0 auto"
+}
+
+// Inset is the padding-<side> declarations (AlignToMain) that put the header's
+// and footer's text at a side column's inner edge: the wrap's own 1em plus
+// the column's share of the wrap's content box, which is the wrap less its two
+// 1em paddings — so (1 - 2w/100)em + w% of the wrap. With a page width the
+// wrap is that width or the screen, whichever is narrower: min() where the
+// browser has it, the page width alone (the wide-screen case) before it.
+func (t Theme) Inset(side string, w int) string {
+ em := strconv.FormatFloat(1-float64(2*w)/100, 'f', -1, 64) + "em"
+ if t.PageWidth == 0 {
+ return "padding-" + side + ":calc(" + em + " + " + strconv.Itoa(w) + "%);"
+ }
+ px := strconv.Itoa(w*t.PageWidth/100) + "px"
+ return "padding-" + side + ":calc(" + em + " + " + px + "); padding-" + side + ":calc(" + em + " + min(" + strconv.Itoa(w) + "%, " + px + "));"
+}
+
// pickFile applies an imagepick or fontpick select: "none" clears, an id
// selects, and an empty value (no select in the form) keeps what was there.
func pickFile(cur, v string) string {
@@ -515,6 +551,12 @@ a:hover {{"{"}}{{if .LinkHover}} color:{{.LinkHover}};{{end}}{{if eq .LinkUnderl
.page-title, .post-title { border-color:{{.LinkColor}}; } .page-title::after, .post-title::after { color:{{.LinkColor}}; }
{{- end}}
.logo { max-height:{{.LogoMaxHeight}}; }
+{{- if .PageWidth}}
+.wrap.body-wrap { max-width:{{.PageWidth}}px; margin:{{.PageMargin}}; }
+{{- if .AlignToMain}}
+.site-header .wrap, .site-footer .wrap { max-width:{{.PageWidth}}px; margin:{{.PageMargin}}; }
+{{- end}}
+{{- end}}
@media (min-width: 701px) {
.col-left { width:{{.LeftWidth}}%; } .col-right { width:{{.RightWidth}}%; }
body.has-left.has-right .col-main { width:{{.MainWidth}}%; }
@@ -524,8 +566,8 @@ a:hover {{"{"}}{{if .LinkHover}} color:{{.LinkHover}};{{end}}{{if eq .LinkUnderl
.site-title { font-size:{{.}}; }
{{- end}}
{{- if .AlignToMain}}
- body.has-left .site-header .wrap, body.has-left .site-footer .wrap { padding-left:calc(1em + {{.LeftWidth}}%); }
- body.has-right .site-header .wrap, body.has-right .site-footer .wrap { padding-right:calc(1em + {{.RightWidth}}%); }
+ body.has-left .site-header .wrap, body.has-left .site-footer .wrap { {{.Inset "left" .LeftWidth}} }
+ body.has-right .site-header .wrap, body.has-right .site-footer .wrap { {{.Inset "right" .RightWidth}} }
{{- end}}
}
{{- with .PaddingPhone}}
diff --git a/internal/web/web_test.go b/internal/web/web_test.go
index ffa9db7..1ca2677 100644
--- a/internal/web/web_test.go
+++ b/internal/web/web_test.go
@@ -251,12 +251,40 @@ func TestLayoutTheme(t *testing.T) {
}
th = ThemeFromForm(th, url.Values{"left_width": {"15"}, "right_width": {"20"}, "align_to_main": {"on"}})
css = th.CSS()
- for _, want := range []string{"body.has-left .site-header .wrap, body.has-left .site-footer .wrap { padding-left:calc(1em + 15%); }",
- "body.has-right .site-header .wrap, body.has-right .site-footer .wrap { padding-right:calc(1em + 20%); }"} {
+ // The inset is the wrap's 1em plus the column's share of the wrap's content box (the wrap less 2em).
+ for _, want := range []string{"body.has-left .site-header .wrap, body.has-left .site-footer .wrap { padding-left:calc(0.7em + 15%); }",
+ "body.has-right .site-header .wrap, body.has-right .site-footer .wrap { padding-right:calc(0.6em + 20%); }"} {
if !strings.Contains(css, want) {
t.Errorf("css missing %q:\n%s", want, css)
}
}
+ if strings.Contains(css, "max-width") {
+ t.Error("no page width, no max-width")
+ }
+ // A page width caps the columns' wrap; the header and footer only when they follow the main column.
+ th = ThemeFromForm(th, url.Values{"left_width": {"15"}, "right_width": {"20"}, "page_width": {"700"}, "page_align": {"left"}})
+ if th.PageWidth != 700 || th.PageAlign != "left" || th.AlignToMain {
+ t.Errorf("page width from form: %+v", th)
+ }
+ css = th.CSS()
+ if !strings.Contains(css, ".wrap.body-wrap { max-width:700px; margin:0; }") || strings.Contains(css, ".site-header .wrap, .site-footer .wrap { max-width") {
+ t.Errorf("page width css: %s", css)
+ }
+ th = ThemeFromForm(th, url.Values{"left_width": {"15"}, "right_width": {"20"}, "page_width": {"700"}, "align_to_main": {"on"}})
+ css = th.CSS()
+ for _, want := range []string{".wrap.body-wrap { max-width:700px; margin:0 auto; }", ".site-header .wrap, .site-footer .wrap { max-width:700px; margin:0 auto; }",
+ "{ padding-left:calc(0.7em + 105px); padding-left:calc(0.7em + min(15%, 105px)); }", "{ padding-right:calc(0.6em + 140px); padding-right:calc(0.6em + min(20%, 140px)); }"} {
+ if !strings.Contains(css, want) {
+ t.Errorf("css missing %q:\n%s", want, css)
+ }
+ }
+ th = ThemeFromForm(th, url.Values{"page_width": {"12"}, "page_align": {"diagonal"}})
+ if th.PageWidth != 400 || th.PageAlign != "center" {
+ t.Errorf("page width clamp: %+v", th)
+ }
+ if th = ThemeFromForm(th, url.Values{"page_width": {""}}); th.PageWidth != 0 {
+ t.Errorf("blank is the screen: %+v", th)
+ }
th.LeftOn = false
if th.AreaOn("left") || !th.AreaOn("above") || !th.AreaOn("bogus") {
t.Errorf("area switches: %+v", th)