fix(card): auto-shrink card titles that don't fit at 15 px (#10)

productive-weekday's "Commits by Weekday (last year, UTC+7.00)" is 40
chars — at font-size 15 that renders ≈360 px wide, past the 340 px
frame. Before the previous stress-test run missed it because the
fixture left UTCOffsetLabel blank, so the title was the short
"Commits by Weekday (last year)" form that happens to fit.

Fix at the render layer instead of rewriting every long title:
header() now picks the largest font between 11 and 15 px at which the
title still fits in 316 px (card width − 20 left inset − 4 right
safety). 40-char titles land at 13 px; ≤ 35-char titles still render at
15 px as before. Readability floor is 11 px — far from the point at
which text becomes illegible, but a hard floor nevertheless.

Stress-test fixture now populates UTCOffsetLabel with "UTC+12.75"
(half-hour / quarter-hour zones make the widest title) so this class
of bug surfaces automatically from here on.
This commit is contained in:
tiennm99 authored and GitHub committed 2026-04-19 09:44:12 +07:00
1 parent 927926751a
commit 9dcb4505fa
2 files changed
+35 -3

No files matched your search

+2
View File
@@ -290,6 +290,8 @@ func adversarialProfile() *github.Profile {
p := &github.Profile{
Login: "user-with-a-very-long-login-name",
Name: "A Very Long Display Name That Keeps Going",
UTCOffsetLabel: "UTC+12.75", // half-hour / quarter-hour zones widen the title
Company: "A-Company-With-An-Unusually-Long-Name Pty Ltd",
Location: "A Place With A Name That Is Way Too Long To Fit",
Website: "https://example-with-a-very-long-domain.example.com/profile",
+33 -3
View File
@@ -3,6 +3,7 @@ package card
import (
"fmt"
"strings"
"unicode/utf8"
)
// escapeXML replaces the five XML-significant characters so user-controlled
@@ -65,14 +66,43 @@ func formatInt(n int) string {
return out
}
// header returns the opening <svg> tag + background rect + title text.
// header returns the opening <svg> tag + background rect + title text. The
// title font-size is auto-shrunk if the string wouldn't fit on one line at
// the default 15 px — productive-time and productive-weekday append the
// timezone label ("UTC+7.00") which can push the title to ~40 chars, past
// the 340 px frame at 15 px. Floor at 11 px so small zones stay readable.
func header(width, height int, bg, stroke string, strokeOpacity float64, titleColor, title string) string {
fontSize := fitTitleFontSize(title, width)
return fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d" font-family="'Segoe UI', Ubuntu, Sans-Serif">
<rect x="0.5" y="0.5" width="%d" height="%d" rx="6" fill="%s" stroke="%s" stroke-opacity="%.2f"/>
<text x="20" y="30" font-size="15" font-weight="600" fill="%s">%s</text>`,
<text x="20" y="30" font-size="%d" font-weight="600" fill="%s">%s</text>`,
width, height, width, height,
width-1, height-1, bg, stroke, strokeOpacity,
titleColor, escapeXML(title))
fontSize, titleColor, escapeXML(title))
}
// fitTitleFontSize picks the largest title font (between 11 and 15 px) at
// which the title still fits in width − leftInset − rightSafety. Uses the
// same 0.6 × font-size char-width estimate as the fit-the-frame test.
func fitTitleFontSize(title string, width int) int {
const (
leftInset = 20
rightSafety = 4
minFont = 11
maxFont = 15
avgCharRatio = 0.6
)
budget := float64(width - leftInset - rightSafety)
chars := utf8.RuneCountInString(title)
if chars == 0 {
return maxFont
}
for fs := maxFont; fs >= minFont; fs-- {
if float64(chars)*float64(fs)*avgCharRatio <= budget {
return fs
}
}
return minFont
}
const footer = `