aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/templates/dashboard/design.html
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/templates/dashboard/design.html')
-rw-r--r--internal/web/templates/dashboard/design.html435
1 files changed, 372 insertions, 63 deletions
diff --git a/internal/web/templates/dashboard/design.html b/internal/web/templates/dashboard/design.html
index e961388..261eb69 100644
--- a/internal/web/templates/dashboard/design.html
+++ b/internal/web/templates/dashboard/design.html
@@ -1,31 +1,44 @@
{{define "title"}}{{t "Design"}} · {{.Blog.Title}}{{end}}
{{define "content"}}
{{$t := .Data.theme}}
+{{$sub := .Blog.Subdomain}}
<div class="pagehead">
<h1>{{t "Design"}}</h1>
- <p class="lead muted">{{t "Changes are live as soon as you save; refresh"}} <a href="{{.BlogURL}}" target="_blank">{{t "your blog"}} &#8599;</a> {{t "to see them."}}</p>
+ <p class="lead muted">{{t "Everything about how your blog looks and what goes where, saved together with the one button at the bottom. Changes are live as soon as you save; refresh"}} <a href="{{.BlogURL}}" target="_blank">{{t "your blog"}} &#8599;</a> {{t "to see them."}}</p>
</div>
-<p class="jumps"><a href="#schemes">{{t "Schemes"}}</a><a href="#background">{{t "Background"}}</a><a href="#text">{{t "Text"}}</a><a href="#header">{{t "Header"}}</a><a href="#menu">{{t "Menu"}}</a><a href="#posts">{{t "Posts"}}</a><a href="#footer">{{t "Footer"}}</a><a href="#logo">{{t "Logo & icon"}}</a></p>
-<p class="muted small">{{t "What appears where — header, columns, footer and their modules — is arranged on the"}} <a href="/b/{{.Blog.Subdomain}}/layout">{{t "Layout"}}</a> {{t "tab."}}</p>
-<div class="card" id="schemes">
- <h2>{{t "Colour schemes"}}</h2>
- <p class="muted small">{{t "A quick start: applying a scheme sets every colour below and saves immediately. Images, fonts and layout are kept."}}</p>
- <form method="post" action="/b/{{.Blog.Subdomain}}/design/preset" class="presets">
- <input type="hidden" name="_csrf" value="{{.CSRF}}">
- {{range .Data.presets}}<button type="submit" name="preset" value="{{.Key}}" class="preset" title="{{tf "Apply %s" (t .Name)}}">
+<div class="design">
+<nav class="sidenav">
+ <a href="#colours">{{t "Colours & fonts"}}</a>
+ <a href="#header">{{t "Header"}}</a>
+ <a href="#menu">{{t "Menu"}}</a>
+ <a href="#content">{{t "Content"}}</a>
+ <a href="#columns">{{t "Side columns"}}</a>
+ <a href="#footer">{{t "Footer"}}</a>
+ <a href="#logo">{{t "Logo & icon"}}</a>
+</nav>
+
+<form method="post" action="/b/{{$sub}}/design" enctype="multipart/form-data" id="design" data-next="{{.Data.nextIndex}}" data-sub="{{$sub}}">
+<input type="hidden" name="_csrf" value="{{.CSRF}}">
+<input type="hidden" name="at" value="">
+
+<div class="card" id="colours">
+ <h2>{{t "Colours & fonts"}}</h2>
+ <h3>{{t "Colour schemes"}}</h3>
+ <p class="muted small">{{t "A quick start: a scheme fills in every colour below. Images, fonts and layout are kept."}}</p>
+ <div class="presets jsonly">
+ {{range .Data.presets}}<button type="button" class="preset" data-colors="{{.Colors}}" title="{{tf "Apply %s" (t .Name)}}">
<span class="sw" style="background:{{.HeaderBg}}"></span><span class="sw" style="background:{{.Bg}}"></span><span class="sw" style="background:{{.ContentBg}}"></span><span class="sw" style="background:{{.Link}}"></span>
<span class="pname">{{t .Name}}</span>
</button>
{{end}}
- </form>
-</div>
-
-<form method="post" action="/b/{{.Blog.Subdomain}}/design" enctype="multipart/form-data">
-<input type="hidden" name="_csrf" value="{{.CSRF}}">
+ </div>
+ <label class="nojs">{{t "Apply a scheme when saving"}}<br><select name="preset">
+ <option value="">{{t "Keep the colours below"}}</option>
+ {{range .Data.presets}}<option value="{{.Key}}">{{t .Name}}</option>{{end}}
+ </select></label>
-<div class="card" id="background">
- <h2>{{t "Background"}}</h2>
+ <h3>{{t "Page background"}}</h3>
<div class="row">
<label>{{t "Colour"}}<br><input type="color" name="bg_color" value="{{$t.BgColor}}"></label>
<label>{{t "Image style"}}<br><select name="bg_mode">
@@ -34,21 +47,25 @@
<option value="tile"{{if eq $t.BgMode "tile"}} selected{{end}}>{{t "Repeat (tile)"}}</option>
</select></label>
</div>
- {{template "imagepick" (dict "name" "bg_image" "current" $t.BgImage "sub" .Blog.Subdomain "images" .Data.images "label" (t "Background image"))}}
-</div>
+ {{template "imagepick" (dict "name" "bg_image" "current" $t.BgImage "sub" $sub "images" .Data.images "names" .Data.imageNames "label" (t "Background image"))}}
-<div class="card" id="text">
- <h2>{{t "Text & content area"}}</h2>
+ <h3>{{t "Text & links"}}</h3>
<div class="row">
- <label>{{t "Content background"}}<br><input type="color" name="content_bg" value="{{$t.ContentBg}}"></label>
<label>{{t "Text colour"}}<br><input type="color" name="text_color" value="{{$t.TextColor}}"></label>
<label>{{t "Link colour"}}<br><input type="color" name="link_color" value="{{$t.LinkColor}}"></label>
+ <label>{{t "Underline links"}}<br><select name="link_underline">
+ <option value="always"{{if eq $t.LinkUnderline "always"}} selected{{end}}>{{t "Always"}}</option>
+ <option value="hover"{{if eq $t.LinkUnderline "hover"}} selected{{end}}>{{t "Only on hover"}}</option>
+ <option value="never"{{if eq $t.LinkUnderline "never"}} selected{{end}}>{{t "Never"}}</option>
+ </select></label>
</div>
<div class="hoverpick">
<input type="checkbox" name="link_hover_custom" id="link-hover-custom"{{if $t.LinkHover}} checked{{end}}>
<label for="link-hover-custom" class="check">{{t "Change colour when the mouse is over a link"}}</label>
<label class="hover-color">{{t "Hover colour"}}<br><input type="color" name="link_hover" value="{{if $t.LinkHover}}{{$t.LinkHover}}{{else}}{{$t.LinkColor}}{{end}}"></label>
</div>
+
+ <h3>{{t "Fonts"}}</h3>
<div class="row">
<label>{{t "Font"}}<br><select name="font">
<option value="sans"{{if eq $t.Font "sans"}} selected{{end}}>Sans-serif (Helvetica/Arial)</option>
@@ -66,28 +83,12 @@
<option value="normal"{{if eq $t.FontSize "normal"}} selected{{end}}>{{t "Normal"}}</option>
<option value="large"{{if eq $t.FontSize "large"}} selected{{end}}>{{t "Large"}}</option>
</select></label>
- <label>{{t "Underline links"}}<br><select name="link_underline">
- <option value="always"{{if eq $t.LinkUnderline "always"}} selected{{end}}>{{t "Always"}}</option>
- <option value="hover"{{if eq $t.LinkUnderline "hover"}} selected{{end}}>{{t "Only on hover"}}</option>
- <option value="never"{{if eq $t.LinkUnderline "never"}} selected{{end}}>{{t "Never"}}</option>
- </select></label>
</div>
- <div class="row">
- <label>{{t "Box style"}}<br><select name="content_style">
- <option value="card"{{if eq $t.ContentStyle "card"}} selected{{end}}>{{t "Card (shadow, rounded corners)"}}</option>
- <option value="flat"{{if eq $t.ContentStyle "flat"}} selected{{end}}>{{t "Flat"}}</option>
- </select></label>
- <label>{{t "Padding"}} <span class="muted">({{t "inside the box"}})</span><br><select name="content_padding">
- <option value="compact"{{if eq $t.ContentPadding "compact"}} selected{{end}}>{{t "Compact"}}</option>
- <option value="normal"{{if eq $t.ContentPadding "normal"}} selected{{end}}>{{t "Normal"}}</option>
- <option value="roomy"{{if eq $t.ContentPadding "roomy"}} selected{{end}}>{{t "Roomy"}}</option>
- </select></label>
- </div>
- <p class="muted small">{{t "The content spans the whole page; how much of it the posts get is set by the column widths on the"}} <a href="/b/{{.Blog.Subdomain}}/layout#columns">{{t "Layout"}}</a> {{t "tab."}}</p>
</div>
<div class="card" id="header">
<h2>{{t "Header"}}</h2>
+ <label class="check"><input type="checkbox" name="header_on" data-area="header"{{if $t.HeaderOn}} checked{{end}}> {{t "Show the header"}} <span class="muted">({{t "the top of every page"}})</span></label>
<div class="row">
<label>{{t "Background"}}<br><input type="color" name="header_bg" value="{{$t.HeaderBg}}"></label>
<label>{{t "Text colour"}}<br><input type="color" name="header_text" value="{{$t.HeaderText}}"></label>
@@ -95,19 +96,44 @@
<option value="left"{{if eq $t.HeaderAlign "left"}} selected{{end}}>{{t "Left"}}</option>
<option value="center"{{if eq $t.HeaderAlign "center"}} selected{{end}}>{{t "Centred"}}</option>
</select></label>
- <label>{{t "Image height"}}<br><select name="header_image_height">
- <option value="short"{{if eq $t.HeaderImageHeight "short"}} selected{{end}}>{{t "Short"}}</option>
- <option value="medium"{{if eq $t.HeaderImageHeight "medium"}} selected{{end}}>{{t "Medium"}}</option>
- <option value="tall"{{if eq $t.HeaderImageHeight "tall"}} selected{{end}}>{{t "Tall"}}</option>
+ <label>{{t "Title size"}}<br><select name="title_size">
+ <option value="small"{{if eq $t.TitleSize "small"}} selected{{end}}>{{t "Small"}}</option>
+ <option value="normal"{{if eq $t.TitleSize "normal"}} selected{{end}}>{{t "Normal"}}</option>
+ <option value="large"{{if eq $t.TitleSize "large"}} selected{{end}}>{{t "Large"}}</option>
</select></label>
</div>
- <p class="muted small">{{t "The title, logo and menu are header modules — add, remove and order them on the"}} <a href="/b/{{.Blog.Subdomain}}/layout#header">{{t "Layout"}}</a> {{t "tab."}} {{t "The banner below is shown across the top of the header whenever one is chosen."}}</p>
- {{template "imagepick" (dict "name" "header_image" "current" $t.HeaderImage "sub" .Blog.Subdomain "images" .Data.images "label" (t "Header image (banner)"))}}
+ {{template "imagepick" (dict "name" "header_image" "current" $t.HeaderImage "sub" $sub "images" .Data.images "names" .Data.imageNames "label" (t "Banner image") "hint" (t "Shown across the top of the header whenever one is chosen."))}}
+ <div class="row"><label>{{t "Banner height"}}<br><select name="header_image_height">
+ <option value="short"{{if eq $t.HeaderImageHeight "short"}} selected{{end}}>{{t "Short"}}</option>
+ <option value="medium"{{if eq $t.HeaderImageHeight "medium"}} selected{{end}}>{{t "Medium"}}</option>
+ <option value="tall"{{if eq $t.HeaderImageHeight "tall"}} selected{{end}}>{{t "Tall"}}</option>
+ </select></label></div>
+ {{template "arealist" (dict "area" "header" "root" . "hint" (t "Stack the title, a logo, the menu and a search box in the order you like."))}}
</div>
<div class="card" id="menu">
<h2>{{t "Menu"}}</h2>
- <p class="muted small">{{t "Which links the menu shows, and where it sits, is set on the"}} <a href="/b/{{.Blog.Subdomain}}/layout#menu">{{t "Layout"}}</a> {{t "tab."}} {{t "Here you choose how it looks."}}</p>
+ <p class="muted small">{{t "The links the Menu module shows, wherever you place it. Pages and outside links can be mixed in any order. Ticking “Show in menu” on a page does the same as adding it here."}}</p>
+ <ul class="modlist plain" id="menulist">
+ {{range .Data.menuRows}}{{template "menurow" (dict "i" .I "item" .Item "pos" .Pos)}}{{end}}
+ </ul>
+ <p class="muted empty"{{if .Data.menuRows}} hidden{{end}}>{{t "The menu is empty."}}</p>
+ <div class="addrow cols">
+ <div>
+ <h3>{{t "Add a page"}}</h3>
+ {{if .Data.freePages}}<label>{{t "Page"}}<br><select name="menu_add_page"><option value="">—</option>{{range .Data.freePages}}<option value="{{.ID}}">{{.Title}}</option>{{end}}</select></label>
+ <button type="button" class="secondary jsonly" data-menu="page">{{t "Add page"}}</button>
+ {{end}}<p class="muted small nopages"{{if .Data.freePages}} hidden{{end}}>{{t "Every page is already in the menu."}}</p>
+ </div>
+ <div>
+ <h3>{{t "Add a link"}}</h3>
+ <label>{{t "Text"}}<br><input name="menu_add_label" maxlength="60" placeholder="{{t "My photos"}}"></label>
+ <label>{{t "Address"}}<br><input name="menu_add_url" maxlength="500" placeholder="https://example.org"></label>
+ <button type="button" class="secondary jsonly" data-menu="link">{{t "Add link"}}</button>
+ </div>
+ </div>
+ <p class="muted small nojs">{{t "A page or link filled in here is added when you save."}}</p>
+ <h3>{{t "How the menu looks"}}</h3>
<div class="row">
<label>{{t "Alignment"}} <span class="muted">({{t "menu in the header"}})</span><br><select name="nav_align">
<option value="left"{{if eq $t.NavAlign "left"}} selected{{end}}>{{t "Left"}}</option>
@@ -118,9 +144,6 @@
<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>
</select></label>
- </div>
- <h3>{{t "Menu colours"}}</h3>
- <div class="row">
<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>
@@ -131,8 +154,22 @@
</div>
</div>
-<div class="card" id="posts">
- <h2>{{t "Posts & dates"}}</h2>
+<div class="card" id="content">
+ <h2>{{t "Content"}}</h2>
+ <p class="muted small">{{t "The box your posts and pages are shown in. It spans whatever the side columns leave."}}</p>
+ <div class="row">
+ <label>{{t "Background"}}<br><input type="color" name="content_bg" value="{{$t.ContentBg}}"></label>
+ <label>{{t "Box style"}}<br><select name="content_style">
+ <option value="card"{{if eq $t.ContentStyle "card"}} selected{{end}}>{{t "Card (shadow, rounded corners)"}}</option>
+ <option value="flat"{{if eq $t.ContentStyle "flat"}} selected{{end}}>{{t "Flat"}}</option>
+ </select></label>
+ <label>{{t "Padding"}} <span class="muted">({{t "inside the box"}})</span><br><select name="content_padding">
+ <option value="compact"{{if eq $t.ContentPadding "compact"}} selected{{end}}>{{t "Compact"}}</option>
+ <option value="normal"{{if eq $t.ContentPadding "normal"}} selected{{end}}>{{t "Normal"}}</option>
+ <option value="roomy"{{if eq $t.ContentPadding "roomy"}} selected{{end}}>{{t "Roomy"}}</option>
+ </select></label>
+ </div>
+ <h3>{{t "Posts"}}</h3>
<label class="check"><input type="checkbox" name="show_dates"{{if $t.ShowDates}} checked{{end}}> {{t "Show the date on posts"}}</label>
<div class="row">
<label>{{t "Date format"}}<br><select name="date_format">
@@ -140,43 +177,315 @@
<option value="short"{{if eq $t.DateFormat "short"}} selected{{end}}>{{postdate "short" .Data.today}}</option>
<option value="iso"{{if eq $t.DateFormat "iso"}} selected{{end}}>{{postdate "iso" .Data.today}}</option>
</select></label>
+ <label>{{t "Posts per page"}}<br><select name="posts_per_page">
+ {{range .Data.perPage}}<option value="{{.}}"{{if eq . $t.PostsPerPage}} selected{{end}}>{{.}}</option>{{end}}
+ </select></label>
</div>
+ <h3>{{t "Around the posts"}}</h3>
+ <p class="muted small">{{t "Your own HTML before and after the posts — an intro box, a newsletter form, a badge…"}}</p>
+ {{template "arealist" (dict "area" "above" "root" .)}}
+ {{template "arealist" (dict "area" "below" "root" .)}}
+</div>
+
+<div class="card" id="columns">
+ <h2>{{t "Side columns"}}</h2>
+ <div class="lay">
+ <div class="lay-row lay-header{{if not $t.HeaderOn}} off{{end}}">{{t "Header"}}</div>
+ <div class="lay-cols">
+ <div class="lay-col lay-side lay-left{{if not $t.LeftOn}} off{{end}}{{if not (index .Data.rows "left")}} empty{{end}}" style="width:{{$t.LeftWidth}}%">{{t "Left"}} <span class="pct">{{$t.LeftWidth}}</span>%</div>
+ <div class="lay-col lay-main" style="width:{{$t.MainWidth}}%">{{t "Main content"}} <span class="pct">{{$t.MainWidth}}</span>%</div>
+ <div class="lay-col lay-side lay-right{{if not $t.RightOn}} off{{end}}{{if not (index .Data.rows "right")}} empty{{end}}" style="width:{{$t.RightWidth}}%">{{t "Right"}} <span class="pct">{{$t.RightWidth}}</span>%</div>
+ </div>
+ <div class="lay-row lay-footer{{if not $t.FooterOn}} off{{end}}">{{t "Footer"}}</div>
+ </div>
+ <div class="row">
+ <label>{{t "Left column width"}}<br><input type="number" name="left_width" value="{{$t.LeftWidth}}" min="15" max="40" step="5"> %</label>
+ <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>
+ <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>
+ <h3>{{t "Left column"}}</h3>
+ <label class="check"><input type="checkbox" name="left_on" data-area="left"{{if $t.LeftOn}} checked{{end}}> {{t "Show the left column"}}</label>
+ {{template "arealist" (dict "area" "left" "root" . "hint" (t "A narrow column beside your posts. Good for the menu, an archive or recent posts."))}}
+ <h3>{{t "Right column"}}</h3>
+ <label class="check"><input type="checkbox" name="right_on" data-area="right"{{if $t.RightOn}} checked{{end}}> {{t "Show the right column"}}</label>
+ {{template "arealist" (dict "area" "right" "root" . "hint" (t "A narrow column on the other side of your posts. Same choices as the left one."))}}
</div>
<div class="card" id="footer">
<h2>{{t "Footer"}}</h2>
- <p class="muted small">{{t "The footer text, RSS link and other footer modules live on the"}} <a href="/b/{{.Blog.Subdomain}}/layout#footer">{{t "Layout"}}</a> {{t "tab."}}</p>
+ <label class="check"><input type="checkbox" name="footer_on" data-area="footer"{{if $t.FooterOn}} checked{{end}}> {{t "Show the footer"}} <span class="muted">({{t "the bottom of every page"}})</span></label>
<div class="row">
<label>{{t "Background"}}<br><input type="color" name="footer_bg" value="{{$t.FooterBg}}"></label>
<label>{{t "Text colour"}}<br><input type="color" name="footer_color" value="{{$t.FooterColor}}"></label>
+ <label>{{t "Alignment"}}<br><select name="footer_align">
+ <option value="center"{{if eq $t.FooterAlign "center"}} selected{{end}}>{{t "Centred"}}</option>
+ <option value="left"{{if eq $t.FooterAlign "left"}} selected{{end}}>{{t "Left"}}</option>
+ </select></label>
</div>
+ {{template "arealist" (dict "area" "footer" "root" . "hint" (t "A copyright line, the RSS link, a list of your pages."))}}
</div>
<div class="card" id="logo">
<h2>{{t "Logo & site icon"}}</h2>
- <div class="row">
- <label>{{t "Logo size"}}<br><select name="logo_size">
- <option value="small"{{if eq $t.LogoSize "small"}} selected{{end}}>{{t "Small (48 px tall)"}}</option>
- <option value="medium"{{if eq $t.LogoSize "medium"}} selected{{end}}>{{t "Medium (80 px)"}}</option>
- <option value="large"{{if eq $t.LogoSize "large"}} selected{{end}}>{{t "Large (140 px)"}}</option>
- </select></label>
- </div>
- {{template "imagepick" (dict "name" "logo" "current" $t.Logo "sub" .Blog.Subdomain "images" .Data.images "label" (t "Logo") "hint" (t "Shown wherever you place a Logo module (Layout tab); it links to your front page."))}}
- {{template "imagepick" (dict "name" "favicon" "current" $t.Favicon "sub" .Blog.Subdomain "images" .Data.images "label" (t "Favicon") "hint" (t "The small icon browsers show in the tab. A square PNG or ICO of 32–64 px works best; pick None for the Blogspace icon.") "accept" "image/png,image/x-icon,image/gif,image/webp,image/jpeg,.ico")}}
+ {{template "imagepick" (dict "name" "logo" "current" $t.Logo "sub" $sub "images" .Data.images "names" .Data.imageNames "label" (t "Logo") "hint" (t "Shown wherever you place a Logo module; it links to your front page."))}}
+ <div class="row"><label>{{t "Logo size"}}<br><select name="logo_size">
+ <option value="small"{{if eq $t.LogoSize "small"}} selected{{end}}>{{t "Small (48 px tall)"}}</option>
+ <option value="medium"{{if eq $t.LogoSize "medium"}} selected{{end}}>{{t "Medium (80 px)"}}</option>
+ <option value="large"{{if eq $t.LogoSize "large"}} selected{{end}}>{{t "Large (140 px)"}}</option>
+ </select></label></div>
+ {{template "imagepick" (dict "name" "favicon" "current" $t.Favicon "sub" $sub "images" .Data.images "names" .Data.imageNames "label" (t "Favicon") "hint" (t "The small icon browsers show in the tab. A square PNG or ICO of 32–64 px works best; pick None for the Blogspace icon.") "accept" "image/png,image/x-icon,image/gif,image/webp,image/jpeg,.ico")}}
</div>
<div class="savebar">
- <button type="submit">{{t "Save design"}}</button>
- <a class="btn secondary" href="{{.BlogURL}}" target="_blank">{{t "View blog"}} &#8599;</a>
+ <button type="submit">{{t "Save"}}</button>
+ <a class="btn secondary" href="/b/{{$sub}}/design" id="discard">{{t "Discard changes"}}</a>
+ <span class="unsaved" hidden>{{t "Unsaved changes"}}</span>
+ <a class="viewlink" href="{{.BlogURL}}" target="_blank">{{t "View blog"}} &#8599;</a>
</div>
+
+{{/* Row templates the script clones: one per area and kind, and the two menu entries. */}}
+{{range .Data.templates}}<template id="tpl-{{.Area}}-{{.Kind}}">{{template "modrow" (dict "i" "__I__" "m" .M "pos" 0 "root" $)}}</template>
+{{end}}<template id="tpl-menu-page">{{template "menurow" (dict "i" "__I__" "item" .Data.pageItem "pos" 0)}}</template>
+<template id="tpl-menu-link">{{template "menurow" (dict "i" "__I__" "item" .Data.linkItem "pos" 0)}}</template>
</form>
<details class="help reset">
<summary>{{t "Reset to defaults"}}</summary>
<p>{{t "Puts every colour, font and layout option back to how a new blog starts and forgets the chosen images (they stay in your library)."}}</p>
- <form method="post" action="/b/{{.Blog.Subdomain}}/design/reset">
+ <form method="post" action="/b/{{$sub}}/design/reset">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<button type="submit" class="danger">{{t "Reset everything"}}</button>
</form>
</details>
+</div>
+
+{{/* The library panel: filled by the script from /files?kind=image as JSON, one page at a time. */}}
+<div class="picker" id="picker" hidden>
+ <div class="picker-box">
+ <div class="picker-head">
+ <input type="search" placeholder="{{t "Filter by name…"}}" aria-label="{{t "Filter by name…"}}">
+ <span class="muted small pcount">{{tf "%d images" .Data.imageCount}}</span>
+ <button type="button" class="secondary small" data-pick="close">{{t "Close"}}</button>
+ </div>
+ <div class="picks"></div>
+ <p class="muted small pnone" hidden>{{t "No images match."}}</p>
+ <div class="pager"><button type="button" class="mini" data-pick="prev">&larr; {{t "Newer"}}</button> <span class="pnum"></span> <button type="button" class="mini" data-pick="next">{{t "Older"}} &rarr;</button></div>
+ </div>
+</div>
+
+<script>
+(function () {
+ var form = document.getElementById("design");
+ if (!form || !("content" in document.createElement("template")) || !form.classList || !form.closest) return;
+ form.className += " js";
+ var next = +form.getAttribute("data-next") || 0, dirty = false, sending = false;
+ function q(sel, el) { return (el || document).querySelector(sel); }
+ function qa(sel, el) { return Array.prototype.slice.call((el || document).querySelectorAll(sel)); }
+ function touch() { dirty = true; q(".unsaved").hidden = false; }
+ form.addEventListener("input", touch);
+ form.addEventListener("change", touch);
+ window.addEventListener("beforeunload", function (e) { if (dirty && !sending) { e.preventDefault(); e.returnValue = ""; } });
+ q("#discard").addEventListener("click", function (e) { if (dirty && !confirm({{t "Throw away your unsaved changes?"}})) e.preventDefault(); else dirty = false; });
+
+ // ---- module and menu rows: order, remove, add
+ function rows(list) { return qa(".modrow", list); }
+ function refresh(list) { // the empty note and the "add" control of a one-module area
+ var n = rows(list).length, card = list.parentNode;
+ var empty = q(".empty", card); if (empty) empty.hidden = n > 0;
+ var add = q(".addmod", card); if (add && list.getAttribute("data-max")) add.hidden = n >= +list.getAttribute("data-max");
+ }
+ function clone(id) { // a row template with a fresh index in its field names
+ var tpl = document.getElementById(id);
+ if (!tpl) return null;
+ var frag = document.importNode(tpl.content, true), row = q(".modrow", frag), i = next++;
+ qa("[name]", row).forEach(function (el) { el.name = el.name.replace("__I__", i); });
+ return row;
+ }
+ form.addEventListener("click", function (e) {
+ var b = e.target.closest("[data-act],[data-menu],[data-pick],.preset");
+ if (!b) return;
+ var row = b.closest(".modrow"), list = row && row.parentNode;
+ switch (b.getAttribute("data-act")) {
+ case "up": if (row.previousElementSibling) list.insertBefore(row, row.previousElementSibling); touch(); return;
+ case "down": if (row.nextElementSibling) list.insertBefore(row.nextElementSibling, row); touch(); return;
+ case "remove":
+ if (row.getAttribute("data-confirm") && !confirm({{t "Remove this module and its text?"}})) return;
+ list.removeChild(row); refresh(list); touch(); return;
+ }
+ switch (b.getAttribute("data-menu")) {
+ case "page":
+ var sel = q("[name=menu_add_page]"), opt = sel && sel.options[sel.selectedIndex];
+ if (!opt || !opt.value) return;
+ var pr = clone("tpl-menu-page");
+ q("[data-field=page]", pr).value = opt.value; q(".modname", pr).textContent = opt.text;
+ q("#menulist").appendChild(pr); sel.removeChild(opt); sel.selectedIndex = 0;
+ q(".nopages", sel.parentNode).hidden = sel.options.length > 1;
+ refresh(q("#menulist")); touch(); return;
+ case "link":
+ var lab = q("[name=menu_add_label]"), url = q("[name=menu_add_url]");
+ if (!lab.value.trim() || !url.value.trim()) { (lab.value.trim() ? url : lab).focus(); return; }
+ var lr = clone("tpl-menu-link");
+ q("[data-field=label]", lr).value = lab.value.trim(); q("[data-field=url]", lr).value = url.value.trim();
+ q("#menulist").appendChild(lr); lab.value = url.value = "";
+ refresh(q("#menulist")); touch(); return;
+ }
+ if (b.className.indexOf("preset") >= 0) { // fill the colour inputs; nothing is saved yet
+ var c = JSON.parse(b.getAttribute("data-colors"));
+ for (var k in c) { var inp = q("[name=" + k + "]", form); if (inp) inp.value = c[k]; }
+ q("[name=link_hover_custom]").checked = q("[name=nav_hover_custom]").checked = false;
+ touch(); return;
+ }
+ pickAction(b);
+ });
+ form.addEventListener("change", function (e) {
+ var t = e.target;
+ if (t.getAttribute("data-add") && t.value) { // a chosen kind becomes a row at once
+ var list = q(".modlist", t.closest(".arealist")), row = clone("tpl-" + list.getAttribute("data-area") + "-" + t.value);
+ if (row) { list.appendChild(row); var d = q("details", row); if (d) d.open = true; refresh(list); }
+ t.selectedIndex = 0;
+ }
+ if (t.getAttribute("data-area")) q(".arealist[data-area=" + t.getAttribute("data-area") + "]").classList.toggle("area-off", !t.checked);
+ });
+ // a heading or text typed into a row shows in its folded summary
+ form.addEventListener("input", function (e) {
+ var t = e.target, row = t.closest(".modrow");
+ if (!row || !t.getAttribute("data-sum")) return;
+ var s = q(".modsum", row); if (s) s.textContent = t.value.replace(/\s+/g, " ").slice(0, 60);
+ });
+ qa(".modlist").forEach(refresh);
+ // the column diagram follows the width boxes
+ function widths() {
+ var l = +q("[name=left_width]").value || 0, r = +q("[name=right_width]").value || 0, m = 100 - l - r;
+ q(".lay-left").style.width = l + "%"; q(".lay-right").style.width = r + "%"; q(".lay-main").style.width = m + "%";
+ q(".lay-left .pct").textContent = l; q(".lay-right .pct").textContent = r; q(".lay-main .pct").textContent = m;
+ }
+ q("[name=left_width]").addEventListener("input", widths);
+ q("[name=right_width]").addEventListener("input", widths);
+ // the side index marks the section in view
+ var cards = qa(".card[id]", form), links = qa(".sidenav a"), spy;
+ function section() {
+ var y = window.pageYOffset + 120, at = cards[0];
+ cards.forEach(function (c) { if (c.offsetTop <= y) at = c; });
+ return at.id;
+ }
+ function mark() { var id = section(); links.forEach(function (a) { a.className = a.getAttribute("href") === "#" + id ? "active" : ""; }); }
+ window.addEventListener("scroll", function () { clearTimeout(spy); spy = setTimeout(mark, 80); });
+ mark();
+
+ form.addEventListener("submit", function () {
+ sending = true;
+ qa(".modlist").forEach(function (list) { rows(list).forEach(function (r, i) { q(".pos", r).value = i; }); });
+ q("[name=at]").value = section(); // so the page comes back to the section in view
+ });
+
+ // ---- the image library panel: one shared panel, fetched page by page
+ var panel = document.getElementById("picker"), pick = null, page = 1, filter = "", timer;
+ if (!window.fetch) { qa(".imagepick .jsonly").forEach(function (el) { el.hidden = true; }); qa(".imagepick select").forEach(function (el) { el.className = ""; }); return; }
+ function pickAction(b) {
+ var act = b.getAttribute("data-pick"), ip = b.closest(".imagepick");
+ if (act === "open") { pick = ip; panel.hidden = false; q("input", panel).value = filter = ""; load(1); q("input", panel).focus(); }
+ else if (act === "clear") { choose(ip, "none", ""); }
+ }
+ panel.addEventListener("click", function (e) {
+ var b = e.target.closest("[data-pick],.pick");
+ if (!b) return;
+ if (b.className.indexOf("pick") === 0) { choose(pick, b.getAttribute("data-id"), b.getAttribute("data-name")); panel.hidden = true; return; }
+ switch (b.getAttribute("data-pick")) {
+ case "close": panel.hidden = true; break;
+ case "prev": load(page - 1); break;
+ case "next": load(page + 1); break;
+ }
+ });
+ panel.addEventListener("keydown", function (e) { if (e.key === "Escape") panel.hidden = true; });
+ q("input", panel).addEventListener("input", function () { clearTimeout(timer); timer = setTimeout(function () { filter = q("input", panel).value.trim(); load(1); }, 250); });
+ function load(n) {
+ var url = "/b/" + form.getAttribute("data-sub") + "/files?kind=image&p=" + n + (filter ? "&q=" + encodeURIComponent(filter) : "");
+ fetch(url, { headers: { Accept: "application/json" }, credentials: "same-origin" }).then(function (r) { return r.json(); }).then(function (d) {
+ page = d.page;
+ var grid = q(".picks", panel); grid.innerHTML = "";
+ (d.files || []).forEach(function (f) {
+ var b = document.createElement("button"), img = document.createElement("img"), s = document.createElement("span");
+ b.type = "button"; b.className = "pick"; b.setAttribute("data-id", f.id); b.setAttribute("data-name", f.filename);
+ img.loading = "lazy"; img.src = "/b/" + form.getAttribute("data-sub") + "/media/" + f.id; img.alt = "";
+ s.className = "name"; s.textContent = f.filename;
+ b.appendChild(img); b.appendChild(s); grid.appendChild(b);
+ });
+ q(".pnone", panel).hidden = !!(d.files && d.files.length);
+ q(".pnum", panel).textContent = d.last > 1 ? d.page + " / " + d.last : "";
+ q("[data-pick=prev]", panel).disabled = d.page <= 1; q("[data-pick=next]", panel).disabled = d.page >= d.last;
+ });
+ }
+ function choose(ip, id, name) { // the select stays the value the form sends
+ var sel = q("select", ip), pv = q(".preview", ip), found = false;
+ for (var i = 0; i < sel.options.length; i++) if (sel.options[i].value === id) found = true;
+ if (!found) { var o = document.createElement("option"); o.value = id; o.textContent = name; sel.appendChild(o); }
+ sel.value = id;
+ pv.innerHTML = id === "none" ? '<span class="swatch">&times;</span>' : "";
+ if (id !== "none") { var img = document.createElement("img"); img.src = "/b/" + form.getAttribute("data-sub") + "/media/" + id; img.alt = ""; pv.appendChild(img); }
+ touch();
+ }
+})();
+</script>
{{end}}
+
+{{/* One area's modules: the list, its empty note and the "add" select. */}}
+{{define "arealist"}}{{$r := .root}}{{$area := .area}}{{$rows := index $r.Data.rows $area}}{{$single := index $r.Data.single $area}}
+<div class="arealist{{if not ($r.Data.theme.AreaOn $area)}} area-off{{end}}" data-area="{{$area}}">
+ {{if $single}}<h4>{{t (index $r.Data.areaNames $area)}}</h4>{{end}}
+ {{with .hint}}<p class="muted small">{{.}}</p>{{end}}
+ <ul class="modlist plain" data-area="{{$area}}"{{if $single}} data-max="1"{{end}}>
+ {{range $rows}}{{template "modrow" (dict "i" .I "m" .M "pos" .Pos "root" $r)}}{{end}}
+ </ul>
+ <p class="muted small empty"{{if $rows}} hidden{{end}}>{{if $single}}{{t "Nothing extra."}}{{else if eq $area "header"}}{{t "Nothing here yet."}}{{else}}{{t "Nothing here yet — this area is not shown until you add a module."}}{{end}}</p>
+ <div class="addmod"{{if and $single $rows}} hidden{{end}}>
+ <label>{{if $single}}{{t "Add custom HTML"}}{{else}}{{t "Add a module"}}{{end}}<br><select name="add.{{$area}}" data-add="1"><option value="">—</option>{{range index $r.Data.kinds $area}}<option value="{{.}}">{{t (index $r.Data.names .)}}</option>{{end}}</select></label>
+ </div>
+</div>{{end}}
+
+{{/* One module row. .i is the field index (or __I__ in a template), .m the module. */}}
+{{define "modrow"}}{{$i := .i}}{{$m := .m}}{{$r := .root}}
+<li class="modrow" data-kind="{{$m.Kind}}"{{if $m.Body}} data-confirm="1"{{end}}>
+ <input type="hidden" name="mod.{{$i}}.id" value="{{$m.ID}}"><input type="hidden" name="mod.{{$i}}.area" value="{{$m.Area}}"><input type="hidden" name="mod.{{$i}}.kind" value="{{$m.Kind}}">
+ <span class="order"><button type="button" class="mini jsonly" data-act="up" title="{{t "Move up"}}">&#9650;</button><button type="button" class="mini jsonly" data-act="down" title="{{t "Move down"}}">&#9660;</button><input type="number" class="nojs pos" name="mod.{{$i}}.pos" value="{{.pos}}" min="0" title="{{t "Position"}}"></span>
+ <span class="modname">{{t (index $r.Data.names $m.Kind)}}</span>
+ {{if and (eq $m.Kind "logo") (not $r.Data.theme.Logo)}}<span class="tag off">{{t "no logo chosen"}}</span> <a class="small" href="#logo">{{t "pick one"}}</a>{{end}}
+ <span class="modact"><label class="check nojs"><input type="checkbox" name="mod.{{$i}}.del"> {{t "Remove"}}</label><button type="button" class="mini danger jsonly" data-act="remove" title="{{t "Remove"}}">&#10005;</button></span>
+ {{if modsettings $m}}<details class="modset">
+ <summary>{{t "Settings"}} <span class="muted modsum">{{modsummary $m}}</span></summary>
+ {{if or (ne $m.Kind "html") (and (ne $m.Area "above") (ne $m.Area "below"))}}{{if ne $m.Kind "text"}}
+ <label>{{t "Heading"}} <span class="muted">({{t "optional"}})</span><br><input name="mod.{{$i}}.title" value="{{$m.Title}}" maxlength="120" data-sum="1"></label>
+ {{end}}{{end}}
+ {{if eq $m.Kind "recent"}}
+ <div class="row"><label>{{t "How many posts"}}<br><select name="mod.{{$i}}.count">{{range $n := $r.Data.counts}}<option value="{{$n}}"{{if eq $n $m.Count}} selected{{end}}>{{$n}}</option>{{end}}</select></label></div>
+ <p class="muted small">{{t "The newest published posts from every page of your blog, newest first."}}</p>
+ {{else if eq $m.Kind "archive"}}
+ <p class="muted small">{{t "Every published post grouped by year and month, newest first. Years and months fold open and shut; the latest year starts open."}}</p>
+ {{else if eq $m.Kind "sitemap"}}
+ <p class="muted small">{{t "Links to every page of your blog."}}</p>
+ {{else if eq $m.Kind "tags"}}
+ <div class="row"><label>{{t "How many tags"}}<br><select name="mod.{{$i}}.count"><option value="0"{{if eq $m.Count 0}} selected{{end}}>{{t "All"}}</option>{{range $n := $r.Data.tagCounts}}<option value="{{$n}}"{{if eq $n $m.Count}} selected{{end}}>{{$n}}</option>{{end}}</select></label></div>
+ <p class="muted small">{{t "Your tags with the number of posts in each, most used first. Only published posts count."}}</p>
+ {{else if eq $m.Kind "tagcloud"}}
+ <p class="muted small">{{t "Every tag in alphabetical order, bigger the more posts use it."}}</p>
+ {{else if eq $m.Kind "search"}}
+ <p class="muted small">{{t "A box that searches the published posts of every page by title and text; the results open on your blog’s search page. Case does not matter, and several words find posts that contain them in that order, with anything in between."}}</p>
+ {{else if eq $m.Kind "html"}}
+ <label>HTML<br><textarea name="mod.{{$i}}.body" rows="6" class="mono" spellcheck="false" placeholder="&lt;p&gt;{{t "Hello!"}}&lt;/p&gt;" data-sum="1">{{$m.Body}}</textarea></label>
+ <p class="muted small">{{t "Put into your page exactly as written — embeds, badges and scripts all work. Anything broken here (an unclosed tag, say) can break the page around it, so check your blog after saving. 20 KB at most."}}</p>
+ {{else if eq $m.Kind "text"}}
+ <label>{{t "Text"}}<br><textarea name="mod.{{$i}}.body" rows="3" placeholder="{{t "© 2026 Me"}}" data-sum="1">{{$m.Body}}</textarea></label>
+ <p class="muted small">{{t "Plain text; each line becomes its own paragraph."}}</p>
+ {{end}}
+ </details>{{end}}
+</li>{{end}}
+
+{{/* One menu entry: a page (fixed) or a link (text and address editable). */}}
+{{define "menurow"}}{{$i := .i}}{{$it := .item}}
+<li class="modrow menurow">
+ <input type="hidden" name="menu.{{$i}}.id" value="{{$it.ID}}">
+ <span class="order"><button type="button" class="mini jsonly" data-act="up" title="{{t "Move up"}}">&#9650;</button><button type="button" class="mini jsonly" data-act="down" title="{{t "Move down"}}">&#9660;</button><input type="number" class="nojs pos" name="menu.{{$i}}.pos" value="{{.pos}}" min="0" title="{{t "Position"}}"></span>
+ {{if $it.PageID}}<input type="hidden" name="menu.{{$i}}.page" value="{{deref64 $it.PageID}}" data-field="page"><span class="modname">{{$it.PageTitle}}</span> <span class="tag">{{t "page"}}</span>
+ {{else}}<span class="tag">{{t "link"}}</span><span class="linkfields"><input name="menu.{{$i}}.label" value="{{$it.Label}}" maxlength="60" placeholder="{{t "Text"}}" data-field="label"><input name="menu.{{$i}}.url" value="{{$it.URL}}" maxlength="500" placeholder="https://example.org" data-field="url"></span>{{end}}
+ <span class="modact"><label class="check nojs"><input type="checkbox" name="menu.{{$i}}.del"> {{t "Remove"}}</label><button type="button" class="mini danger jsonly" data-act="remove" title="{{t "Remove"}}">&#10005;</button></span>
+</li>{{end}}