feat(web): sleek Stage-0 reader — design system, card list, reader detail

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.
This commit is contained in:
2026-06-03 00:20:22 +02:00
parent fa9f101abe
commit d9b7107ccb
5 changed files with 339 additions and 286 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ func (a *App) handleList(w http.ResponseWriter, r *http.Request) {
rows = f.apply(rows) rows = f.apply(rows)
if isHTMX(r) { if isHTMX(r) {
a.render(w, r, summaryTable(rows)) a.render(w, r, summaryList(rows))
return return
} }
a.render(w, r, ListPage(rows, f)) a.render(w, r, ListPage(rows, f))
+3 -3
View File
@@ -149,7 +149,7 @@ func TestListRendersRowsAndActionState(t *testing.T) {
require.Contains(t, html, "youtube") require.Contains(t, html, "youtube")
require.Contains(t, html, "watched", "current action state shown in the list") require.Contains(t, html, "watched", "current action state shown in the list")
require.Contains(t, html, "2026-01-01") 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) { func TestListHTMXReturnsFragment(t *testing.T) {
@@ -164,8 +164,8 @@ func TestListHTMXReturnsFragment(t *testing.T) {
require.Equal(t, http.StatusOK, rec.Code) require.Equal(t, http.StatusOK, rec.Code)
html := body(t, rec) html := body(t, rec)
require.Contains(t, html, "<table") require.Contains(t, html, `class="cards"`)
require.NotContains(t, html, "<html", "HTMX request gets only the table fragment") require.NotContains(t, html, "<html", "HTMX request gets only the list fragment")
} }
func TestListChannelFilter(t *testing.T) { func TestListChannelFilter(t *testing.T) {
+112 -23
View File
@@ -1,6 +1,7 @@
package web package web
import ( import (
"strings"
"time" "time"
"github.com/a-h/templ" "github.com/a-h/templ"
@@ -65,12 +66,38 @@ func displayTitle(r store.SummaryRow) string {
return r.VideoID return r.VideoID
} }
// displayDate renders a published date, or an em dash when absent (zero time). // cardMeta is the muted "channel · date" line on a list card. Empty parts are
func displayDate(t time.Time) string { // omitted so a row with no joined videos row renders no stray separators.
if t.IsZero() { func cardMeta(r store.SummaryRow) string {
return "—" var parts []string
if r.Channel != "" {
parts = append(parts, r.Channel)
} }
return t.Format("2006-01-02") if !r.PublishedAt.IsZero() {
parts = append(parts, r.PublishedAt.Format("2006-01-02"))
}
return strings.Join(parts, " · ")
}
// detailMeta is the single-line meta join under the detail title. It builds a
// slice of the present parts (channel, date, provider[+model]) and joins with
// " · ", so an absent videos row never produces a dangling "· — ·" (review #5).
// The fallback badge is rendered separately, not part of this string.
func detailMeta(r store.SummaryRow) string {
var parts []string
if r.Channel != "" {
parts = append(parts, r.Channel)
}
if !r.PublishedAt.IsZero() {
parts = append(parts, r.PublishedAt.Format("2006-01-02"))
}
if prov := r.AIProvider; prov != "" {
if r.AIModel != "" {
prov += " (" + r.AIModel + ")"
}
parts = append(parts, prov)
}
return strings.Join(parts, " · ")
} }
// videoURL builds the internal detail-page path for a video id. // videoURL builds the internal detail-page path for a video id.
@@ -149,25 +176,87 @@ func (f Filter) apply(rows []store.SummaryRow) []store.SummaryRow {
// not as template expressions — so the CSS is rendered as a raw node instead). // not as template expressions — so the CSS is rendered as a raw node instead).
var styleTag = "<style>" + stylesheet + "</style>" var styleTag = "<style>" + stylesheet + "</style>"
// 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 = ` 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; } * { box-sizing: border-box; }
body { font: 15px/1.5 system-ui, sans-serif; margin: 0; color: var(--fg); } body { font: 15px/1.6 system-ui, -apple-system, sans-serif; margin: 0; color: var(--fg); background: var(--bg); }
header { padding: .75rem 1.25rem; border-bottom: 1px solid var(--line); } a { color: var(--accent); text-decoration: none; }
.brand { font-weight: 700; text-decoration: none; color: var(--accent); } a:hover, a:focus-visible { text-decoration: underline; }
main { max-width: 60rem; margin: 0 auto; padding: 1.25rem; } a:visited { color: var(--accent); }
table.summaries { width: 100%; border-collapse: collapse; } header { padding: var(--s3) var(--s4); border-bottom: 1px solid var(--line); background: var(--card); }
table.summaries th, table.summaries td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } .brand { font-weight: 700; font-size: 1.05rem; color: var(--accent); }
table.summaries th { font-size: .8rem; text-transform: uppercase; letter-spacing: .03em; color: var(--muted); } main { max-width: 60rem; margin: 0 auto; padding: var(--s4) var(--s3); }
.empty { color: var(--muted); font-style: italic; }
.muted { color: var(--muted); } .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; } /* filter bar */
.filters label { display: flex; flex-direction: column; font-size: .8rem; color: var(--muted); gap: .2rem; } .filters { display: flex; gap: var(--s3); align-items: end; flex-wrap: wrap; margin-bottom: var(--s4); }
.filters input { font: inherit; padding: .25rem; } .filters label { display: flex; flex-direction: column; font-size: .78rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); gap: var(--s1); }
.detail .meta { color: var(--muted); } .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; }
.actions { display: flex; gap: .5rem; margin: 1rem 0; } .filters input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.actions .action { font: inherit; padding: .4rem .8rem; border: 1px solid var(--line); border-radius: .4rem; background: transparent; cursor: pointer; } .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; }
.actions .action.active { background: var(--accent); color: #fff; border-color: var(--accent); } .btn:hover { filter: brightness(1.05); }
.body { white-space: pre-wrap; } .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); }
}
` `
+37 -43
View File
@@ -34,7 +34,7 @@ templ ListPage(rows []store.SummaryRow, f Filter) {
@Layout("Tapir — Summaries") { @Layout("Tapir — Summaries") {
@filterForm(f) @filterForm(f)
<div id="summary-list"> <div id="summary-list">
@summaryTable(rows) @summaryList(rows)
</div> </div>
} }
} }
@@ -47,54 +47,49 @@ templ filterForm(f Filter) {
hx-get="/" hx-get="/"
hx-target="#summary-list" hx-target="#summary-list"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-indicator="#filter-indicator"
> >
<label>Channel <input type="text" name="channel" value={ f.Channel } placeholder="any"/></label> <label>Channel <input type="text" name="channel" value={ f.Channel } placeholder="any"/></label>
<label>From <input type="date" name="from" value={ f.From }/></label> <label>From <input type="date" name="from" value={ f.From }/></label>
<label>To <input type="date" name="to" value={ f.To }/></label> <label>To <input type="date" name="to" value={ f.To }/></label>
<button type="submit">Filter</button> <button type="submit" class="btn">Filter</button>
<span id="filter-indicator" class="htmx-indicator">filtering…</span>
</form> </form>
} }
// summaryTable is the swappable list fragment: title · channel · published · // summaryList is the swappable list fragment: one card per summary (title link,
// provider · fallback badge · current action state. // channel · date meta, provider chip, fallback badge, action state). Cards
templ summaryTable(rows []store.SummaryRow) { // reflow to a single column on mobile; an empty list shows a friendly first-run
<table class="summaries"> // state instead of a blank table.
<thead> templ summaryList(rows []store.SummaryRow) {
<tr>
<th>Title</th>
<th>Channel</th>
<th>Published</th>
<th>Provider</th>
<th></th>
<th>Actions</th>
</tr>
</thead>
<tbody>
if len(rows) == 0 { if len(rows) == 0 {
<tr><td colspan="6" class="empty">No summaries.</td></tr> <div class="empty">
} <strong>No summaries yet</strong>
for _, r := range rows { <span>Summaries appear here as your subscriptions are processed run <code>tapir run</code> to fetch and summarize new videos.</span>
<tr> </div>
<td><a href={ videoURL(r.VideoID) }>{ displayTitle(r) }</a></td>
<td>{ r.Channel }</td>
<td>{ displayDate(r.PublishedAt) }</td>
<td>{ r.AIProvider }</td>
<td>
if r.FallbackUsed {
<span class="badge">fallback</span>
}
</td>
<td class="state">
if len(r.Actions) == 0 {
<span class="muted"></span>
} else { } else {
{ strings.Join(r.Actions, ", ") } <ul class="cards">
for _, r := range rows {
<li class="card">
<div class="card-title"><a href={ videoURL(r.VideoID) }>{ displayTitle(r) }</a></div>
if cardMeta(r) != "" {
<div class="card-meta">{ cardMeta(r) }</div>
} }
</td> <div class="card-foot">
</tr> if r.AIProvider != "" {
<span class="chip">{ r.AIProvider }</span>
}
if r.FallbackUsed {
<span class="badge" title="summarized with the fallback model" aria-label="summarized with the fallback model">fallback</span>
}
if len(r.Actions) > 0 {
<span class="card-state">{ strings.Join(r.Actions, ", ") }</span>
}
</div>
</li>
}
</ul>
} }
</tbody>
</table>
} }
// DetailPage is the full summary view: text, highlights, takeaways, metadata, // DetailPage is the full summary view: text, highlights, takeaways, metadata,
@@ -104,16 +99,15 @@ templ DetailPage(r store.SummaryRow) {
<article class="detail"> <article class="detail">
<h1>{ displayTitle(r) }</h1> <h1>{ displayTitle(r) }</h1>
<p class="meta"> <p class="meta">
{ r.Channel } · { displayDate(r.PublishedAt) } · { r.AIProvider } if detailMeta(r) != "" {
if r.AIModel != "" { <span>{ detailMeta(r) }</span>
{ "(" + r.AIModel + ")" }
} }
if r.FallbackUsed { if r.FallbackUsed {
<span class="badge">fallback</span> <span class="badge" title="summarized with the fallback model" aria-label="summarized with the fallback model">fallback</span>
} }
</p> </p>
if r.URL != "" { if r.URL != "" {
<p><a href={ externalURL(r.URL) } rel="noopener noreferrer">watch on source </a></p> <p class="source"><a href={ externalURL(r.URL) } rel="noopener noreferrer">watch on source </a></p>
} }
@ActionButtons(r.VideoID, actionSet(r.Actions)) @ActionButtons(r.VideoID, actionSet(r.Actions))
<section> <section>
+135 -165
View File
@@ -118,7 +118,7 @@ func ListPage(rows []store.SummaryRow, f Filter) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -157,14 +157,14 @@ func filterForm(f Filter) templ.Component {
templ_7745c5c3_Var5 = templ.NopComponent templ_7745c5c3_Var5 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<form class=\"filters\" method=\"get\" action=\"/\" hx-get=\"/\" hx-target=\"#summary-list\" hx-swap=\"innerHTML\"><label>Channel <input type=\"text\" name=\"channel\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<form class=\"filters\" method=\"get\" action=\"/\" hx-get=\"/\" hx-target=\"#summary-list\" hx-swap=\"innerHTML\" hx-indicator=\"#filter-indicator\"><label>Channel <input type=\"text\" name=\"channel\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Channel) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Channel)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 51, Col: 68} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 52, Col: 68}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -177,7 +177,7 @@ func filterForm(f Filter) templ.Component {
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.From) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.From)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 52, Col: 59} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 53, Col: 59}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -190,13 +190,13 @@ func filterForm(f Filter) templ.Component {
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.To) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.To)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 53, Col: 53} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 54, Col: 53}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"></label> <button type=\"submit\">Filter</button></form>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"></label> <button type=\"submit\" class=\"btn\">Filter</button> <span id=\"filter-indicator\" class=\"htmx-indicator\">filtering…</span></form>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -204,9 +204,11 @@ func filterForm(f Filter) templ.Component {
}) })
} }
// summaryTable is the swappable list fragment: title · channel · published · // summaryList is the swappable list fragment: one card per summary (title link,
// provider · fallback badge · current action state. // channel · date meta, provider chip, fallback badge, action state). Cards
func summaryTable(rows []store.SummaryRow) templ.Component { // 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) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -227,25 +229,25 @@ func summaryTable(rows []store.SummaryRow) templ.Component {
templ_7745c5c3_Var9 = templ.NopComponent templ_7745c5c3_Var9 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<table class=\"summaries\"><thead><tr><th>Title</th><th>Channel</th><th>Published</th><th>Provider</th><th></th><th>Actions</th></tr></thead> <tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(rows) == 0 { if len(rows) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<tr><td colspan=\"6\" class=\"empty\">No summaries.</td></tr>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"empty\"><strong>No summaries yet</strong> <span>Summaries appear here as your subscriptions are processed — run <code>tapir run</code> to fetch and summarize new videos.</span></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<ul class=\"cards\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
} }
for _, r := range rows { for _, r := range rows {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<tr><td><a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<li class=\"card\"><div class=\"card-title\"><a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var10 templ.SafeURL var templ_7745c5c3_Var10 templ.SafeURL
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(videoURL(r.VideoID)) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(videoURL(r.VideoID))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 78, Col: 38} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 74, Col: 58}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -258,90 +260,93 @@ func summaryTable(rows []store.SummaryRow) templ.Component {
var templ_7745c5c3_Var11 string var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 78, Col: 58} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 74, Col: 78}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</a></td><td>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if cardMeta(r) != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"card-meta\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var12 string var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(r.Channel) templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(cardMeta(r))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 79, Col: 20} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</td><td>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"card-foot\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if r.AIProvider != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"chip\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var13 string var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(displayDate(r.PublishedAt)) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(r.AIProvider)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 80, Col: 37} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</td><td>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if r.FallbackUsed {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span class=\"badge\" title=\"summarized with the fallback model\" aria-label=\"summarized with the fallback model\">fallback</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(r.Actions) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span class=\"card-state\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(r.AIProvider) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(r.Actions, ", "))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 81, Col: 23} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td><td>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if r.FallbackUsed {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"badge\">fallback</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td><td class=\"state\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(r.Actions) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span class=\"muted\">—</span>")
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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</td></tr>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</ul>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</tbody></table>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil return nil
}) })
} }
@@ -364,12 +369,12 @@ func DetailPage(r store.SummaryRow) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var16 := templ.GetChildren(ctx) templ_7745c5c3_Var15 := templ.GetChildren(ctx)
if templ_7745c5c3_Var16 == nil { if templ_7745c5c3_Var15 == nil {
templ_7745c5c3_Var16 = templ.NopComponent templ_7745c5c3_Var15 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer { if !templ_7745c5c3_IsBuffer {
@@ -381,79 +386,44 @@ func DetailPage(r store.SummaryRow) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<article class=\"detail\"><h1>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<article class=\"detail\"><h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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: 100, Col: 24}
}
_, 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, 27, "</h1><p class=\"meta\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if detailMeta(r) != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var18 string var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(detailMeta(r))
if templ_7745c5c3_Err != nil { 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: 103, Col: 26}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</h1><p class=\"meta\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</span> ")
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 templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
if r.FallbackUsed { if r.FallbackUsed {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<span class=\"badge\">fallback</span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<span class=\"badge\" title=\"summarized with the fallback model\" aria-label=\"summarized with the fallback model\">fallback</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -463,16 +433,16 @@ func DetailPage(r store.SummaryRow) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if r.URL != "" { if r.URL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<p><a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<p class=\"source\"><a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var23 templ.SafeURL var templ_7745c5c3_Var19 templ.SafeURL
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(externalURL(r.URL)) templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinURLErrs(externalURL(r.URL))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 116, Col: 35} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 110, Col: 50}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -489,12 +459,12 @@ func DetailPage(r store.SummaryRow) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var24 string var templ_7745c5c3_Var20 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(r.Summary) templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(r.Summary)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 121, Col: 31} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 115, Col: 31}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -512,12 +482,12 @@ func DetailPage(r store.SummaryRow) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var25 string var templ_7745c5c3_Var21 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(h) templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(h)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 128, Col: 14} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 122, Col: 14}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -541,12 +511,12 @@ func DetailPage(r store.SummaryRow) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var26 string var templ_7745c5c3_Var22 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(t) templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(t)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 138, Col: 14} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 132, Col: 14}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -566,7 +536,7 @@ func DetailPage(r store.SummaryRow) templ.Component {
} }
return nil return nil
}) })
templ_7745c5c3_Err = Layout("Tapir — "+displayTitle(r)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var17), templ_7745c5c3_Buffer) templ_7745c5c3_Err = Layout("Tapir — "+displayTitle(r)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var16), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -594,21 +564,21 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var27 := templ.GetChildren(ctx) templ_7745c5c3_Var23 := templ.GetChildren(ctx)
if templ_7745c5c3_Var27 == nil { if templ_7745c5c3_Var23 == nil {
templ_7745c5c3_Var27 = templ.NopComponent templ_7745c5c3_Var23 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<form id=\"action-buttons\" class=\"actions\" method=\"post\" action=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<form id=\"action-buttons\" class=\"actions\" method=\"post\" action=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var28 templ.SafeURL var templ_7745c5c3_Var24 templ.SafeURL
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(actionURL(videoID)) templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(actionURL(videoID))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 156, Col: 29} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 150, Col: 29}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -616,12 +586,12 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var29 string var templ_7745c5c3_Var25 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(string(actionURL(videoID))) templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(string(actionURL(videoID)))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 157, Col: 38} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 151, Col: 38}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -630,8 +600,8 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, v := range actionVerbs { for _, v := range actionVerbs {
var templ_7745c5c3_Var30 = []any{"action", templ.KV("active", active[v])} var templ_7745c5c3_Var26 = []any{"action", templ.KV("active", active[v])}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var30...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var26...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -639,12 +609,12 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var31 string var templ_7745c5c3_Var27 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(v) templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(v)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 165, Col: 13} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 159, Col: 13}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -652,12 +622,12 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var32 string var templ_7745c5c3_Var28 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var30).String()) templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var26).String())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 1, Col: 0} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 1, Col: 0}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -665,12 +635,12 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var33 string var templ_7745c5c3_Var29 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(ariaPressed(active[v])) templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(ariaPressed(active[v]))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 167, Col: 41} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 161, Col: 41}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -679,22 +649,22 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if active[v] { if active[v] {
var templ_7745c5c3_Var34 string var templ_7745c5c3_Var30 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs("✓ " + actionLabel(v)) templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs("✓ " + actionLabel(v))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 170, Col: 30} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 164, Col: 30}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
var templ_7745c5c3_Var35 string var templ_7745c5c3_Var31 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(actionLabel(v)) templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(actionLabel(v))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 172, Col: 21} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 166, Col: 21}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }