diff options
| author | grm <grm@eyesin.space> | 2026-09-17 17:09:01 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-17 17:09:28 +0300 |
| commit | f16bd0f79c624665580615321b5d13b4be27cdad (patch) | |
| tree | 3f7c47fdd8c8ff2e928b04891dfa65ed3ffe8c75 | |
| parent | f79f97800f7f9ba1ef2b3e2e56040a952a9668a5 (diff) | |
| download | blogspace-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.md | 19 | ||||
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | internal/db/migrations/blog/00009_gizmo_style.sql | 10 | ||||
| -rw-r--r-- | internal/i18n/el.go | 4 | ||||
| -rw-r--r-- | internal/store/sections.go | 2 | ||||
| -rw-r--r-- | internal/web/handlers_sections.go | 2 | ||||
| -rw-r--r-- | internal/web/static/blog.css | 14 | ||||
| -rw-r--r-- | internal/web/templates/dashboard/section_form.html | 2 | ||||
| -rw-r--r-- | internal/web/templates/dashboard/sections.html | 2 | ||||
| -rw-r--r-- | internal/web/templates/layouts/blog.html | 4 | ||||
| -rw-r--r-- | internal/web/theme.go | 1 |
11 files changed, 50 insertions, 13 deletions
@@ -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 @@ -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}}%; } |
