aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-17 17:09:01 +0300
committergrm <grm@eyesin.space>2026-09-17 17:09:28 +0300
commitf16bd0f79c624665580615321b5d13b4be27cdad (patch)
tree3f7c47fdd8c8ff2e928b04891dfa65ed3ffe8c75
parentf79f97800f7f9ba1ef2b3e2e56040a952a9668a5 (diff)
downloadblogspace-f16bd0f79c624665580615321b5d13b4be27cdad.tar.gz
blogspace-f16bd0f79c624665580615321b5d13b4be27cdad.tar.bz2
blogspace-f16bd0f79c624665580615321b5d13b4be27cdad.zip
Add the Gizmo look for announcements: framed boxes that share a row
gizmolab's "next event" and "next assembly" boxes were hand-written HTML in the home page's intro; they are announcements by nature, so now they can be: a fourth style, a box framed in the theme's link colour with an italic heading. Headed Gizmo boxes in the same spot share a row on wide screens (the notices wrapper is a flex row, stacking on old browsers like .cols); one without a heading takes the whole row, which is how a banner line goes above a pair of boxes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
-rw-r--r--AGENTS.md19
-rw-r--r--README.md3
-rw-r--r--internal/db/migrations/blog/00009_gizmo_style.sql10
-rw-r--r--internal/i18n/el.go4
-rw-r--r--internal/store/sections.go2
-rw-r--r--internal/web/handlers_sections.go2
-rw-r--r--internal/web/static/blog.css14
-rw-r--r--internal/web/templates/dashboard/section_form.html2
-rw-r--r--internal/web/templates/dashboard/sections.html2
-rw-r--r--internal/web/templates/layouts/blog.html4
-rw-r--r--internal/web/theme.go1
11 files changed, 50 insertions, 13 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 068889b..5753d26 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -70,8 +70,8 @@ internal/web/ server.go (host router, middleware, render helpers)
summary.go (what a listing shows of a post)
templates/ (embedded; layouts/, partials/, auth/, dashboard/, admin/, blog/)
static/ (dashboard.css — lean look; custom properties, flexbox and grid
- are fine there. blog.css — floats except the three-column `.cols`,
- which is flexbox; it must keep working on old browsers)
+ are fine there. blog.css — floats except the three-column `.cols`
+ and the `.notices` row, which are flexbox; it must keep working on old browsers)
```
## Key mechanics
@@ -316,12 +316,19 @@ internal/web/ server.go (host router, middleware, render helpers)
- **Announcements** (`sections` table, `store/sections.go`,
`handlers_sections.go`, `/b/{sub}/announcements…`): per-blog notices with
`placement` (`<column>-<position>`: left|main|right × top|bottom, split by
- `Section.Column()/Position()`), `style` (plain|note|warning), `enabled`
+ `Section.Column()/Position()`), `style` (plain|note|warning|gizmo — a CHECK
+ constraint, so a new style needs a migration), `enabled`
and `sort_order`. `blogView` loads the enabled ones and `placeNotices`
groups them by placement, moving ones for a side column that is not laid
out into the main column.
- Rendered by the `notices` template in `layouts/blog.html`; CSS classes
- `notice notice-<style>` use translucent colours so they fit any theme.
+ Rendered by the `notices` template in `layouts/blog.html` inside a
+ `.notices` wrapper; CSS classes `notice notice-<style>` use translucent
+ colours so they fit any theme, except `gizmo`: a box framed in the theme's
+ link colour (`.notice-gizmo { border-color }` comes from the theme CSS).
+ On wide screens `.notices` is a flex row (the second flexbox in blog.css,
+ stacking on old browsers like `.cols`): headed gizmo boxes (`.headed`)
+ start at 30% and grow, so up to three share a row; every other box takes
+ the whole row.
- **Dashboard nav**: `dashnav.html` marks the current tab with `hasPrefix
.Path`; a new management section needs a tab there.
- **Languages** (`internal/i18n`, `settings.language`): every blog has a
@@ -425,7 +432,7 @@ superadmin password to `admin`. Production refuses both.
`-- +goose Up/Down` sections; they run automatically at startup. Never
edit an applied migration. Blog chain so far: `00001_init`, `00002_language`,
`00003_files`, `00004_tags`, `00005_search`, `00006_format`, `00007_post_image`,
- `00008_page_outro`; control: `00001_init`, `00002_upload_limit`. Both chains were re-baselined at 00001 after
+ `00008_page_outro`, `00009_gizmo_style`; control: `00001_init`, `00002_upload_limit`. Both chains were re-baselined at 00001 after
the move to per-blog databases; deployments from before it have
`goose_db_version` rows 2–7 in the control DB that must be deleted once
(README "Upgrading from a single database") or the next control migration
diff --git a/README.md b/README.md
index df35621..828ff20 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,8 @@ Server-rendered HTML, no JavaScript required, works on old browsers and phones.
them by kind with search, rename and delete. Uploads are 10 MB per file by default;
the superadmin can set a different limit per blog.
- **Announcements**: blog-wide notices (next meeting, this month's book, a closure)
- shown at the top or bottom of the main content or of a side column; each can be hidden without deleting.
+ shown at the top or bottom of the main content or of a side column — highlighted, as a
+ warning, plain, or as framed "Gizmo" boxes that sit side by side; each can be hidden without deleting.
- Every blog has **pages** (Home, About, News, …); each page holds posts, with an
optional text before them (an intro) and after them (a site map, a widget). Posts can
carry **tags**, shown under the date; each tag has its own page listing every post
diff --git a/internal/db/migrations/blog/00009_gizmo_style.sql b/internal/db/migrations/blog/00009_gizmo_style.sql
new file mode 100644
index 0000000..1af8733
--- /dev/null
+++ b/internal/db/migrations/blog/00009_gizmo_style.sql
@@ -0,0 +1,10 @@
+-- +goose Up
+-- A fourth look for announcements: a box framed in the link colour, with
+-- boxes in the same spot sitting side by side on wide screens.
+ALTER TABLE sections DROP CONSTRAINT sections_style_check;
+ALTER TABLE sections ADD CONSTRAINT sections_style_check CHECK (style IN ('plain', 'note', 'warning', 'gizmo'));
+
+-- +goose Down
+UPDATE sections SET style = 'plain' WHERE style = 'gizmo';
+ALTER TABLE sections DROP CONSTRAINT sections_style_check;
+ALTER TABLE sections ADD CONSTRAINT sections_style_check CHECK (style IN ('plain', 'note', 'warning'));
diff --git a/internal/i18n/el.go b/internal/i18n/el.go
index 88610b4..1fd6ceb 100644
--- a/internal/i18n/el.go
+++ b/internal/i18n/el.go
@@ -142,7 +142,9 @@ var el = map[string]string{
"Highlighted": "Τονισμένη",
"Warning": "Προειδοποίηση",
"Plain": "Απλή",
- "Next meeting": "Επόμενη συνάντηση",
+ "Gizmo": "Gizmo",
+ "Gizmo is a box framed in your link colour. Gizmo boxes with a heading share a row on wide screens (up to three); one without a heading takes the whole row.": "Το Gizmo είναι ένα πλαίσιο στο χρώμα των συνδέσμων σου. Τα πλαίσια Gizmo με επικεφαλίδα μοιράζονται μια σειρά σε φαρδιές οθόνες (μέχρι τρία)· ένα χωρίς επικεφαλίδα πιάνει όλη τη σειρά.",
+ "Next meeting": "Επόμενη συνάντηση",
"“Top” of a side column means above its modules, “bottom” below them. If that column is not shown on your blog (see": "«Κορυφή» μιας πλαϊνής στήλης σημαίνει πάνω από τα στοιχεία της, «τέλος» κάτω από αυτά. Αν η στήλη δεν εμφανίζεται στο ιστολόγιό σας (βλ.",
"the announcement moves to the main content instead.": "η ανακοίνωση μεταφέρεται στο κύριο περιεχόμενο.",
"Shown on the blog": "Εμφανίζεται στο ιστολόγιο",
diff --git a/internal/store/sections.go b/internal/store/sections.go
index 2d72ea4..95c6a00 100644
--- a/internal/store/sections.go
+++ b/internal/store/sections.go
@@ -14,7 +14,7 @@ type Section struct {
BodyHTML string
Format string // FormatMarkdown | FormatHTML: how BodyMD is read
Placement string // <column>-<position>: left|main|right - top|bottom
- Style string // plain | note | warning
+ Style string // plain | note | warning | gizmo
Enabled bool
SortOrder int
CreatedAt time.Time
diff --git a/internal/web/handlers_sections.go b/internal/web/handlers_sections.go
index 8d6c9c9..04ba764 100644
--- a/internal/web/handlers_sections.go
+++ b/internal/web/handlers_sections.go
@@ -53,7 +53,7 @@ func (s *Server) handleSectionForm(w http.ResponseWriter, r *http.Request) {
// normalizeSection clamps the enum fields so templates and CSS class names can trust them.
func normalizeSection(sec *store.Section) {
sec.Placement = pick(sec.Column(), "main", "left", "right") + "-" + pick(sec.Position(), "top", "bottom")
- sec.Style = pick(sec.Style, "note", "plain", "warning")
+ sec.Style = pick(sec.Style, "note", "plain", "warning", "gizmo")
}
func (s *Server) handleSectionSave(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/web/static/blog.css b/internal/web/static/blog.css
index ace5e5a..eabb314 100644
--- a/internal/web/static/blog.css
+++ b/internal/web/static/blog.css
@@ -48,7 +48,19 @@ img { max-width: 100%; height: auto; }
.notice-title { margin: 0 0 0.3em; font-size: 1em; text-transform: uppercase; letter-spacing: 0.05em; }
.notice-body > :first-child { margin-top: 0; }
.notice-body > :last-child { margin-bottom: 0; }
-.content > .notice:last-child { margin: 1.5em 0 0; }
+.content > .notices:last-child { margin-top: 1.5em; }
+.content > .notices:last-child > .notice:last-child { margin-bottom: 0; }
+/* gizmo: a box framed in the link colour (set by the theme CSS). On wide
+ screens the flex wrapper puts up to three headed gizmo boxes in one row
+ (they start at 30% and grow); a gizmo box without a heading, like every
+ plain/note/warning box, takes the whole row */
+.notice-gizmo { padding: 1em 1.2em; border: 1px solid; background: none; }
+.notice-gizmo .notice-title { font-size: 1.15em; font-style: italic; text-transform: none; letter-spacing: 0; margin-bottom: 0.6em; }
+@media (min-width: 701px) {
+ .notices { display: flex; flex-wrap: wrap; gap: 0 1em; align-items: stretch; }
+ .notices .notice { flex: 1 1 100%; }
+ .notices .notice-gizmo.headed { flex: 1 1 30%; }
+}
.pager { margin-top: 2em; padding-top: 1em; border-top: 1px solid rgba(0,0,0,0.1); text-align: center; }
.pager a, .pager span { margin: 0 0.8em; }
.muted { opacity: 0.7; }
diff --git a/internal/web/templates/dashboard/section_form.html b/internal/web/templates/dashboard/section_form.html
index 0171aaa..0a0e4ea 100644
--- a/internal/web/templates/dashboard/section_form.html
+++ b/internal/web/templates/dashboard/section_form.html
@@ -20,8 +20,10 @@
<option value="note"{{if eq $s.Style "note"}} selected{{end}}>{{t "Highlighted"}}</option>
<option value="warning"{{if eq $s.Style "warning"}} selected{{end}}>{{t "Warning"}}</option>
<option value="plain"{{if eq $s.Style "plain"}} selected{{end}}>{{t "Plain"}}</option>
+ <option value="gizmo"{{if eq $s.Style "gizmo"}} selected{{end}}>{{t "Gizmo"}}</option>
</select></label>
</div>
+ <p class="muted small">{{t "Gizmo is a box framed in your link colour. Gizmo boxes with a heading share a row on wide screens (up to three); one without a heading takes the whole row."}}</p>
<p class="muted small">{{t "“Top” of a side column means above its modules, “bottom” below them. If that column is not shown on your blog (see"}} <a href="/b/{{.Blog.Subdomain}}/design#columns">{{t "Design"}}</a>) {{t "the announcement moves to the main content instead."}}</p>
<label for="ed-body">{{t "Text"}}</label>
{{template "editor" (dict "name" "body" "value" $s.BodyMD "format" $s.Format "rows" 10 "tall" false "upload" (print "/b/" .Blog.Subdomain "/files/upload") "preview" (print "/b/" .Blog.Subdomain "/preview") "csrf" .CSRF)}}
diff --git a/internal/web/templates/dashboard/sections.html b/internal/web/templates/dashboard/sections.html
index 4d62f29..9f0e314 100644
--- a/internal/web/templates/dashboard/sections.html
+++ b/internal/web/templates/dashboard/sections.html
@@ -17,7 +17,7 @@
</td>
<td><a href="{{$u}}/{{.ID}}/edit">{{if .Title}}{{.Title}}{{else}}({{t "untitled"}}){{end}}</a>{{if not .Enabled}}<span class="tag off">{{t "hidden"}}</span>{{end}}</td>
<td>{{if eq .Column "main"}}{{t "Main content"}}{{else if eq .Column "left"}}{{t "Left column"}}{{else}}{{t "Right column"}}{{end}}, {{t .Position}}</td>
- <td>{{if eq .Style "note"}}{{t "Highlighted"}}{{else if eq .Style "warning"}}{{t "Warning"}}{{else}}{{t "Plain"}}{{end}}</td>
+ <td>{{if eq .Style "note"}}{{t "Highlighted"}}{{else if eq .Style "warning"}}{{t "Warning"}}{{else if eq .Style "gizmo"}}{{t "Gizmo"}}{{else}}{{t "Plain"}}{{end}}</td>
<td class="nowrap">
<form method="post" action="{{$u}}/{{.ID}}/toggle" class="inline"><input type="hidden" name="_csrf" value="{{$.CSRF}}"><button class="mini">{{if .Enabled}}{{t "Hide"}}{{else}}{{t "Show"}}{{end}}</button></form>
<a class="danger" href="{{$u}}/{{.ID}}/delete">{{t "delete"}}</a>
diff --git a/internal/web/templates/layouts/blog.html b/internal/web/templates/layouts/blog.html
index 6e8b819..76475b9 100644
--- a/internal/web/templates/layouts/blog.html
+++ b/internal/web/templates/layouts/blog.html
@@ -43,10 +43,12 @@
</body>
</html>{{end}}
-{{define "notices"}}{{range .}}<aside class="notice notice-{{.Style}}">
+{{define "notices"}}{{if .}}<div class="notices">
+{{range .}}<aside class="notice notice-{{.Style}}{{if .Title}} headed{{end}}">
{{if .Title}}<h3 class="notice-title">{{.Title}}</h3>{{end}}
<div class="notice-body">{{html .BodyHTML}}</div>
</aside>
+{{end}}</div>
{{end}}{{end}}
{{/* One layout module. .m is the store.Module, .root the page data; the same
diff --git a/internal/web/theme.go b/internal/web/theme.go
index 1fc78ad..df2c613 100644
--- a/internal/web/theme.go
+++ b/internal/web/theme.go
@@ -448,6 +448,7 @@ a:hover {{"{"}}{{if .LinkHover}} color:{{.LinkHover}};{{end}}{{if eq .LinkUnderl
{{- end}}
.content, .mod { background:{{.ContentBg}};{{if eq .ContentStyle "flat"}} border-radius:0; box-shadow:none;{{end}} }
.content {{"{"}}{{with .PaddingDesktop}} padding:{{.}};{{end}} }
+.notice-gizmo { border-color:{{.LinkColor}}; }
.logo { max-height:{{.LogoMaxHeight}}; }
@media (min-width: 701px) {
.col-left { width:{{.LeftWidth}}%; } .col-right { width:{{.RightWidth}}%; }