From 19353a51c869f4b24ef2253d856084b6e6728048 Mon Sep 17 00:00:00 2001 From: grm Date: Fri, 18 Sep 2026 12:10:26 +0300 Subject: Pair headed gizmo announcements two to a row, split by text length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Headed gizmo boxes used to share a row equally, up to three of them. Now placeNotices pairs consecutive ones in the main column and gives the pair one of the fixed splits — 25/75, 33/67, 50/50, 67/33, 75/25 — nearest the ratio of the text each shows, so the wordier box gets the room. The bases add up to 90% so a third box never joins the row, and a side column, too narrow for two, stacks them. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A --- internal/web/layout.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 3 deletions(-) (limited to 'internal/web/layout.go') diff --git a/internal/web/layout.go b/internal/web/layout.go index 6f5c84f..91728ed 100644 --- a/internal/web/layout.go +++ b/internal/web/layout.go @@ -1,9 +1,12 @@ package web import ( + "html" + "math" "net/url" "strings" "time" + "unicode/utf8" "github.com/gramanas/blogspace/internal/i18n" "github.com/gramanas/blogspace/internal/store" @@ -243,17 +246,69 @@ func groupArchive(posts []store.PostRef) []ArchiveYear { return years } +// Notice is an announcement placed on the page: the section plus, for a +// headed gizmo box sharing a row with the next one, its share of the row. +type Notice struct { + store.Section + Width int // 0 = the whole row; else one of rowShares, in percent +} + +// rowShares are the widths two boxes on one row can take: the longer text +// gets the bigger share, and the pair always adds up to 100. +var rowShares = []int{25, 33, 50, 67, 75} + // placeNotices groups announcements by placement ("left-top", "main-bottom", …). // Ones meant for a side column that is not laid out go to the main column instead. -func placeNotices(secs []store.Section, l Layout) map[string][]store.Section { - out := map[string][]store.Section{} +// In each group, headed gizmo boxes are paired up, two to a row, and the +// pair's widths are set by how much text each carries (main column only — +// a side column stacks them). +func placeNotices(secs []store.Section, l Layout) map[string][]Notice { + out := map[string][]Notice{} for _, sec := range secs { col := sec.Column() if (col == "left" && !l.HasLeft) || (col == "right" && !l.HasRight) { col = "main" } key := col + "-" + sec.Position() - out[key] = append(out[key], sec) + out[key] = append(out[key], Notice{Section: sec}) + } + for key, ns := range out { + if strings.HasPrefix(key, "main-") { + pairNotices(ns) + } } return out } + +// pairNotices gives every two consecutive headed gizmo boxes their row shares. +func pairNotices(ns []Notice) { + for i := 0; i+1 < len(ns); i++ { + if !ns[i].pairable() || !ns[i+1].pairable() { + continue + } + ns[i].Width, ns[i+1].Width = shareRow(noticeLen(ns[i]), noticeLen(ns[i+1])) + i++ + } +} + +func (n Notice) pairable() bool { return n.Style == "gizmo" && n.Title != "" } + +// noticeLen is how much text a box shows: its title and body, tags stripped. +func noticeLen(n Notice) int { + return utf8.RuneCountInString(n.Title) + utf8.RuneCountInString(html.UnescapeString(stripTags.Sanitize(n.BodyHTML))) +} + +// shareRow picks the pair of rowShares nearest to the two lengths' ratio. +func shareRow(a, b int) (int, int) { + if a+b == 0 { + return 50, 50 + } + want := float64(a) * 100 / float64(a+b) + best := 50 + for _, s := range rowShares { + if math.Abs(float64(s)-want) < math.Abs(float64(best)-want) { + best = s + } + } + return best, 100 - best +} -- cgit v1.2.3