From e4fe39ecc5418d3a2b9f318949f7b06445427db7 Mon Sep 17 00:00:00 2001 From: arnef Date: Sat, 15 Aug 2026 11:12:48 +0200 Subject: [PATCH] Fix book grid layout with long titles - static/styles.css: use CSS grid instead of inline-block for .book-list so rows align consistently regardless of card content height; add overflow-wrap/word-break to titles/authors - views/pages.templ: truncate long titles/authors server-side (truncateTitle helper) instead of relying on CSS line-clamp, which behaved inconsistently across browsers (esp. Firefox with flex); full title kept in title attribute and on the book detail page - views/pages_templ.go regenerated via templ generate Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- static/styles.css | 25 +++++++--- views/pages.templ | 15 +++++- views/pages_templ.go | 114 ++++++++++++++++++++++++++----------------- 3 files changed, 99 insertions(+), 55 deletions(-) diff --git a/static/styles.css b/static/styles.css index 3f23156..2c24649 100644 --- a/static/styles.css +++ b/static/styles.css @@ -73,26 +73,28 @@ h2 { .book-list { list-style: none; - margin: 0 -0.25rem; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; + margin: 0; padding: 0; } .book-item { - display: inline-block; - width: 33.3333%; - vertical-align: top; box-sizing: border-box; - padding: 0.25rem; margin: 0; border: none; } .book-card { - display: block; + display: flex; + flex-direction: column; + height: 100%; border: 1px solid #000; padding: 0.5rem; color: inherit; text-decoration: none; + box-sizing: border-box; } .book-card-cover { @@ -141,18 +143,25 @@ h2 { margin-bottom: 1rem; } -.book-title { - display: block; +.book-card .book-title { font-weight: bold; font-size: 0.95rem; line-height: 1.25; margin-bottom: 0.25rem; + overflow-wrap: break-word; + word-break: break-word; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; } .book-author, .book-author-detail { display: block; margin-top: 0.2rem; + overflow-wrap: break-word; + word-break: break-word; } .book-author { diff --git a/views/pages.templ b/views/pages.templ index 9303c20..0314c24 100644 --- a/views/pages.templ +++ b/views/pages.templ @@ -114,8 +114,8 @@ templ IndexPage(books []library.Book, canUpload bool) { @BookCover(b, false)
- { b.Title } - { b.Author } + { truncateTitle(b.Title, 60) } + { truncateTitle(b.Author, 40) } { b.Format }
@@ -164,3 +164,14 @@ func placeholderClass(detail bool) string { } return "book-cover-placeholder" } + +// truncateTitle kürzt lange Titel für die Kartenansicht, damit lange, +// nicht umbrechbare Titel das Grid-Layout nicht sprengen. Der volle Titel +// bleibt im Detail-Seiten-Titel und im alt-Text des Covers erhalten. +func truncateTitle(title string, maxLen int) string { + r := []rune(title) + if len(r) <= maxLen { + return title + } + return string(r[:maxLen]) + "…" +} diff --git a/views/pages_templ.go b/views/pages_templ.go index 5c0e4ae..4bf9712 100644 --- a/views/pages_templ.go +++ b/views/pages_templ.go @@ -466,51 +466,64 @@ func IndexPage(books []library.Book, canUpload bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var24 string - templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(b.Author) + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(truncateTitle(b.Title, 60)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 118, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 117, Col: 89} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(b.Format) + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(truncateTitle(b.Author, 40)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 119, Col: 47} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 118, Col: 71} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var26 string + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(b.Format) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 119, Col: 47} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -541,12 +554,12 @@ func BookPage(book library.Book) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var26 := templ.GetChildren(ctx) - if templ_7745c5c3_Var26 == nil { - templ_7745c5c3_Var26 = templ.NopComponent + templ_7745c5c3_Var27 := templ.GetChildren(ctx) + if templ_7745c5c3_Var27 == nil { + templ_7745c5c3_Var27 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var27 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var28 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -558,7 +571,7 @@ func BookPage(book library.Book) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -566,78 +579,78 @@ func BookPage(book library.Book) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 136, Col: 24} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author) + templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 137, Col: 51} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 136, Col: 24} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "

Datei
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(book.Filename) + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 141, Col: 31} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 137, Col: 51} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "

Format
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "

Datei
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(book.Format) + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(book.Filename) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 145, Col: 29} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 141, Col: 31} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

Auf Tolino herunterladen

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = Layout(book.Title, "/").Render(templ.WithChildren(ctx, templ_7745c5c3_Var27), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout(book.Title, "/").Render(templ.WithChildren(ctx, templ_7745c5c3_Var28), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -659,4 +672,15 @@ func placeholderClass(detail bool) string { return "book-cover-placeholder" } +// truncateTitle kürzt lange Titel für die Kartenansicht, damit lange, +// nicht umbrechbare Titel das Grid-Layout nicht sprengen. Der volle Titel +// bleibt im Detail-Seiten-Titel und im alt-Text des Covers erhalten. +func truncateTitle(title string, maxLen int) string { + r := []rune(title) + if len(r) <= maxLen { + return title + } + return string(r[:maxLen]) + "…" +} + var _ = templruntime.GeneratedTemplate