From d9b7107ccb19379048ec7409f6caaaf1cba4838a Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 3 Jun 2026 00:20:22 +0200 Subject: [PATCH] =?UTF-8?q?feat(web):=20sleek=20Stage-0=20reader=20?= =?UTF-8?q?=E2=80=94=20design=20system,=20card=20list,=20reader=20detail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the Top 5 fixes from the Stage-0 UX review (docs/ux-review/UX-REVIEW.md): 1. Dark mode: full light+dark custom-property palette (bg/fg/muted/line/accent/ card) under :root + @media (prefers-color-scheme: dark), applied to body. color-scheme: light dark is now actually honoured — summary text was invisible on a dark canvas before. 2. Table -> responsive card list: one card per summary (title link, channel·date meta, provider chip, fallback badge, action state). Single-column reflow at 375px, no horizontal crush. 3. Minimal design system: 4/8px spacing scale, one accent, styled accent links (underline-on-hover), real buttons with active/pressed state, consistent radius and dividers — applied across list + detail. 4. Detail page as a reader: prose capped at 38rem, title->meta->summary-> highlights->takeaways hierarchy with section rules, 1.7 line-height. Meta is built from non-empty parts (detailMeta) so the no-video edge case no longer renders a stray "· — ·". 5. Contrast + a11y: muted bumped to #595959 (~7:1, clears WCAG AA), fallback badge gets vertical padding + aria-label/title, friendly first-run empty state, hx-indicator on the filter form. Tests updated for the card markup (table -> cards); missing date is now omitted rather than em-dashed. task check green; HTMX action toggles verified working. --- internal/web/handlers.go | 2 +- internal/web/handlers_test.go | 6 +- internal/web/view.go | 135 ++++++++++-- internal/web/views.templ | 78 +++---- internal/web/views_templ.go | 404 ++++++++++++++++------------------ 5 files changed, 339 insertions(+), 286 deletions(-) diff --git a/internal/web/handlers.go b/internal/web/handlers.go index c80a589..db1884f 100644 --- a/internal/web/handlers.go +++ b/internal/web/handlers.go @@ -84,7 +84,7 @@ func (a *App) handleList(w http.ResponseWriter, r *http.Request) { rows = f.apply(rows) if isHTMX(r) { - a.render(w, r, summaryTable(rows)) + a.render(w, r, summaryList(rows)) return } a.render(w, r, ListPage(rows, f)) diff --git a/internal/web/handlers_test.go b/internal/web/handlers_test.go index 177be64..eff4a0a 100644 --- a/internal/web/handlers_test.go +++ b/internal/web/handlers_test.go @@ -149,7 +149,7 @@ func TestListRendersRowsAndActionState(t *testing.T) { require.Contains(t, html, "youtube") require.Contains(t, html, "watched", "current action state shown in the list") require.Contains(t, html, "2026-01-01") - require.Contains(t, html, "—", "missing published date renders an em dash") + require.Contains(t, html, `class="card"`, "rows render as cards, not a table") } func TestListHTMXReturnsFragment(t *testing.T) { @@ -164,8 +164,8 @@ func TestListHTMXReturnsFragment(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code) html := body(t, rec) - require.Contains(t, html, "" +// The design system is a small set of CSS custom properties: one accent, a +// 4/8px-derived spacing scale, a single radius, and full light+dark palettes so +// color-scheme: light dark is actually honoured (review #1). Muted is #595959 +// (~7:1 on white) / #9aa0a8 on dark to clear WCAG AA (review #4). const stylesheet = ` -:root { color-scheme: light dark; --fg:#1a1a1a; --muted:#777; --line:#ddd; --accent:#2b6cb0; } +:root { + color-scheme: light dark; + --bg:#fbfbfa; --card:#ffffff; --fg:#1a1a1a; --muted:#595959; --line:#e4e4e1; + --accent:#2b6cb0; --accent-fg:#ffffff; --accent-weak:#eaf1f8; + --badge-bg:#e7a13a; --badge-fg:#2a1d00; + --s1:.25rem; --s2:.5rem; --s3:1rem; --s4:1.5rem; --s5:2.5rem; --radius:.5rem; +} +@media (prefers-color-scheme: dark) { + :root { + --bg:#15171b; --card:#1e2128; --fg:#e7e7e4; --muted:#9aa0a8; --line:#2d313a; + --accent:#76aae6; --accent-fg:#0c0f13; --accent-weak:#222b38; + --badge-bg:#caa24a; --badge-fg:#1a1300; + } +} * { box-sizing: border-box; } -body { font: 15px/1.5 system-ui, sans-serif; margin: 0; color: var(--fg); } -header { padding: .75rem 1.25rem; border-bottom: 1px solid var(--line); } -.brand { font-weight: 700; text-decoration: none; color: var(--accent); } -main { max-width: 60rem; margin: 0 auto; padding: 1.25rem; } -table.summaries { width: 100%; border-collapse: collapse; } -table.summaries th, table.summaries td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -table.summaries th { font-size: .8rem; text-transform: uppercase; letter-spacing: .03em; color: var(--muted); } -.empty { color: var(--muted); font-style: italic; } +body { font: 15px/1.6 system-ui, -apple-system, sans-serif; margin: 0; color: var(--fg); background: var(--bg); } +a { color: var(--accent); text-decoration: none; } +a:hover, a:focus-visible { text-decoration: underline; } +a:visited { color: var(--accent); } +header { padding: var(--s3) var(--s4); border-bottom: 1px solid var(--line); background: var(--card); } +.brand { font-weight: 700; font-size: 1.05rem; color: var(--accent); } +main { max-width: 60rem; margin: 0 auto; padding: var(--s4) var(--s3); } .muted { color: var(--muted); } -.badge { display: inline-block; padding: 0 .4rem; border-radius: .4rem; background: #f0ad4e; color: #000; font-size: .75rem; } -.filters { display: flex; gap: 1rem; align-items: end; flex-wrap: wrap; margin-bottom: 1rem; } -.filters label { display: flex; flex-direction: column; font-size: .8rem; color: var(--muted); gap: .2rem; } -.filters input { font: inherit; padding: .25rem; } -.detail .meta { color: var(--muted); } -.actions { display: flex; gap: .5rem; margin: 1rem 0; } -.actions .action { font: inherit; padding: .4rem .8rem; border: 1px solid var(--line); border-radius: .4rem; background: transparent; cursor: pointer; } -.actions .action.active { background: var(--accent); color: #fff; border-color: var(--accent); } -.body { white-space: pre-wrap; } + +/* filter bar */ +.filters { display: flex; gap: var(--s3); align-items: end; flex-wrap: wrap; margin-bottom: var(--s4); } +.filters label { display: flex; flex-direction: column; font-size: .78rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); gap: var(--s1); } +.filters input { font: inherit; padding: .4rem .55rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--card); color: var(--fg); min-width: 9rem; } +.filters input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); } +.btn { font: inherit; font-weight: 600; padding: .45rem 1rem; border: 1px solid var(--accent); border-radius: var(--radius); background: var(--accent); color: var(--accent-fg); cursor: pointer; } +.btn:hover { filter: brightness(1.05); } +.btn:active { transform: translateY(1px); } + +/* card list */ +.cards { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s3); } +.card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: var(--s3) var(--s4); display: flex; flex-direction: column; gap: var(--s2); } +.card-title { font-size: 1.1rem; font-weight: 600; line-height: 1.3; } +.card-meta { color: var(--muted); font-size: .85rem; } +.card-foot { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; margin-top: var(--s1); } +.chip { display: inline-block; padding: .15rem .55rem; border-radius: 999px; background: var(--accent-weak); color: var(--accent); font-size: .72rem; font-weight: 600; } +.card-state { color: var(--muted); font-size: .8rem; } +.badge { display: inline-block; padding: .15rem .55rem; border-radius: 999px; background: var(--badge-bg); color: var(--badge-fg); font-size: .72rem; font-weight: 600; } + +/* empty state */ +.empty { text-align: center; color: var(--muted); padding: var(--s5) var(--s4); border: 1px dashed var(--line); border-radius: var(--radius); background: var(--card); } +.empty strong { display: block; color: var(--fg); font-size: 1.05rem; margin-bottom: var(--s2); } +.empty code { background: var(--accent-weak); color: var(--accent); padding: .1rem .35rem; border-radius: .3rem; } + +/* htmx loading feedback */ +.htmx-indicator { opacity: 0; transition: opacity .2s; color: var(--muted); font-size: .8rem; } +.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { opacity: 1; } + +/* detail reader */ +.detail { max-width: 38rem; } +.detail h1 { font-size: 1.7rem; line-height: 1.25; margin: 0 0 var(--s2); } +.detail .meta { color: var(--muted); font-size: .9rem; margin: 0 0 var(--s2); display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; } +.detail .source { margin: 0 0 var(--s4); font-size: .9rem; } +.detail section { margin-top: var(--s4); } +.detail section h2 { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); border-top: 1px solid var(--line); padding-top: var(--s3); margin: 0 0 var(--s2); } +.detail .body { white-space: pre-wrap; line-height: 1.7; margin: 0; } +.detail ul { margin: 0; padding-left: 1.2rem; line-height: 1.6; } +.detail li { margin-bottom: var(--s1); } + +/* action toggles */ +.actions { display: flex; gap: var(--s2); margin: var(--s4) 0; flex-wrap: wrap; } +.actions .action { font: inherit; padding: .4rem .9rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--card); color: var(--fg); cursor: pointer; transition: border-color .15s, background .15s; } +.actions .action:hover { border-color: var(--accent); } +.actions .action:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } +.actions .action:active { transform: translateY(1px); } +.actions .action.active { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); } + +@media (max-width: 640px) { + main { padding: var(--s3) var(--s2); } + .filters { gap: var(--s2); } + .filters input { min-width: 0; width: 100%; } + .filters label { flex: 1 1 8rem; } + .card { padding: var(--s3); } +} ` diff --git a/internal/web/views.templ b/internal/web/views.templ index b9e2688..a7134cf 100644 --- a/internal/web/views.templ +++ b/internal/web/views.templ @@ -34,7 +34,7 @@ templ ListPage(rows []store.SummaryRow, f Filter) { @Layout("Tapir — Summaries") { @filterForm(f)
- @summaryTable(rows) + @summaryList(rows)
} } @@ -47,54 +47,49 @@ templ filterForm(f Filter) { hx-get="/" hx-target="#summary-list" hx-swap="innerHTML" + hx-indicator="#filter-indicator" > - + + filtering… } -// summaryTable is the swappable list fragment: title · channel · published · -// provider · fallback badge · current action state. -templ summaryTable(rows []store.SummaryRow) { - - - - - - - - - - - - - if len(rows) == 0 { - - } +// summaryList is the swappable list fragment: one card per summary (title link, +// channel · date meta, provider chip, fallback badge, action state). Cards +// reflow to a single column on mobile; an empty list shows a friendly first-run +// state instead of a blank table. +templ summaryList(rows []store.SummaryRow) { + if len(rows) == 0 { +
+ No summaries yet + Summaries appear here as your subscriptions are processed — run tapir run to fetch and summarize new videos. +
+ } else { + - - - - - - - + + } - -
TitleChannelPublishedProviderActions
No summaries.
{ displayTitle(r) }{ r.Channel }{ displayDate(r.PublishedAt) }{ r.AIProvider } +
  • + + if cardMeta(r) != "" { +
    { cardMeta(r) }
    + } +
    + if r.AIProvider != "" { + { r.AIProvider } + } if r.FallbackUsed { - fallback + fallback } -
  • - if len(r.Actions) == 0 { - - } else { - { strings.Join(r.Actions, ", ") } + if len(r.Actions) > 0 { + { strings.Join(r.Actions, ", ") } } -
    + + } } // DetailPage is the full summary view: text, highlights, takeaways, metadata, @@ -104,16 +99,15 @@ templ DetailPage(r store.SummaryRow) {

    { displayTitle(r) }

    - { r.Channel } · { displayDate(r.PublishedAt) } · { r.AIProvider } - if r.AIModel != "" { - { "(" + r.AIModel + ")" } + if detailMeta(r) != "" { + { detailMeta(r) } } if r.FallbackUsed { - fallback + fallback }

    if r.URL != "" { -

    watch on source ↗

    +

    watch on source ↗

    } @ActionButtons(r.VideoID, actionSet(r.Actions))
    diff --git a/internal/web/views_templ.go b/internal/web/views_templ.go index 3472d21..4bac615 100644 --- a/internal/web/views_templ.go +++ b/internal/web/views_templ.go @@ -118,7 +118,7 @@ func ListPage(rows []store.SummaryRow, f Filter) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = summaryTable(rows).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = summaryList(rows).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -157,14 +157,14 @@ func filterForm(f Filter) templ.Component { templ_7745c5c3_Var5 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
    filtering…") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -204,9 +204,11 @@ func filterForm(f Filter) templ.Component { }) } -// summaryTable is the swappable list fragment: title · channel · published · -// provider · fallback badge · current action state. -func summaryTable(rows []store.SummaryRow) templ.Component { +// summaryList is the swappable list fragment: one card per summary (title link, +// channel · date meta, provider chip, fallback badge, action state). Cards +// reflow to a single column on mobile; an empty list shows a friendly first-run +// state instead of a blank table. +func summaryList(rows []store.SummaryRow) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -227,120 +229,123 @@ func summaryTable(rows []store.SummaryRow) templ.Component { templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } if len(rows) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
    No summaries yet Summaries appear here as your subscriptions are processed — run tapir run to fetch and summarize new videos.
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } - for _, r := range rows { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
    TitleChannelPublishedProviderActions
    No summaries.
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var10 templ.SafeURL - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(videoURL(r.VideoID)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 78, Col: 38} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 78, Col: 58} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(r.Channel) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 79, Col: 20} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(displayDate(r.PublishedAt)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 80, Col: 37} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(r.AIProvider) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 81, Col: 23} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if r.FallbackUsed { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "fallback") + for _, r := range rows { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if cardMeta(r) != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(cardMeta(r)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 76, Col: 42} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if r.AIProvider != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(r.AIProvider) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 80, Col: 40} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if r.FallbackUsed { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "fallback ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(r.Actions) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(r.Actions, ", ")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 86, Col: 63} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
  • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if len(r.Actions) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(r.Actions, ", ")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 91, Col: 38} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
    ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err } return nil }) @@ -364,12 +369,12 @@ func DetailPage(r store.SummaryRow) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var16 := templ.GetChildren(ctx) - if templ_7745c5c3_Var16 == nil { - templ_7745c5c3_Var16 = templ.NopComponent + templ_7745c5c3_Var15 := templ.GetChildren(ctx) + if templ_7745c5c3_Var15 == nil { + templ_7745c5c3_Var15 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var17 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var16 := 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 { @@ -381,79 +386,44 @@ func DetailPage(r store.SummaryRow) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "

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

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 105, Col: 24} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 100, Col: 24} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "

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

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(r.Channel) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 107, Col: 15} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " · ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var20 string - templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(displayDate(r.PublishedAt)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 107, Col: 49} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " · ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(r.AIProvider) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 107, Col: 69} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if r.AIModel != "" { - var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs("(" + r.AIModel + ")") - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 109, Col: 28} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + if detailMeta(r) != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ") + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(detailMeta(r)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 103, Col: 26} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if r.FallbackUsed { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "fallback") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "fallback") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -463,16 +433,16 @@ func DetailPage(r store.SummaryRow) templ.Component { return templ_7745c5c3_Err } if r.URL != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "