From a4aeb5efcd48affa874d73b17a90f1deebf3a3e7 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 3 Jun 2026 20:11:17 +0200 Subject: [PATCH] feat(web): charmbracelet-aesthetic tapir spinner with charm palette MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the plain ASCII spinner with a Charmbracelet-style TUI panel rendered in the browser: a dark terminal card holding a rounded purple box (╭─╮╰─╯│) around a pink block-char tapir (▄▓▀█) with mint eyes (◕ ◕) and a snout that wiggles ∩→∪→~ across three cross-faded frames, plus a lipgloss-style progress bar whose mint fill grows over a dim track via an 8s CSS clip animation. Colours come from named palette consts (CharmPurple/Pink/Mint/Cream/Dim) applied as inline span styles, so the box and CSS share one source of truth. Frames are built by a small run/line helper that pads each row to a fixed interior width — an internal test asserts every box row is the same cell width so the border stays flush. Honours prefers-reduced-motion (static frame + partial bar). Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/web/processing_handler_test.go | 6 +- internal/web/view.go | 125 ++++- internal/web/view_internal_test.go | 23 +- internal/web/views.templ | 17 +- internal/web/views_templ.go | 585 ++++++++++++------------ 5 files changed, 435 insertions(+), 321 deletions(-) diff --git a/internal/web/processing_handler_test.go b/internal/web/processing_handler_test.go index 0b561af..456b3ff 100644 --- a/internal/web/processing_handler_test.go +++ b/internal/web/processing_handler_test.go @@ -48,8 +48,10 @@ func TestRequestSummarizeImmediateProcessing(t *testing.T) { // The processing card came back while ProcessVideo is still parked on block: // the work runs in a goroutine, the handler did not wait for it. require.Contains(t, html, "Summarizing", "processing card returned") - require.Contains(t, html, "ω", "ascii tapir frame rendered") - require.Contains(t, html, "∪∪", "all tapir frames rendered") + require.Contains(t, html, "╭", "charm box rendered") + require.Contains(t, html, "▓", "tapir body block chars rendered") + require.Contains(t, html, "∩", "wiggling snout frame rendered") + require.Contains(t, html, web.CharmPurple, "charm palette applied to the border") require.Contains(t, html, "/v/"+videoX+"/status", "card polls the status endpoint") require.Contains(t, html, `hx-trigger="every 2s"`, "card auto-polls every 2s") require.NotContains(t, html, "Queued", "not the queue-only card") diff --git a/internal/web/view.go b/internal/web/view.go index 7bd5f63..4c9479b 100644 --- a/internal/web/view.go +++ b/internal/web/view.go @@ -4,6 +4,7 @@ import ( "regexp" "strings" "time" + "unicode/utf8" "github.com/a-h/templ" @@ -182,22 +183,87 @@ func statusURL(videoID string) templ.SafeURL { return templ.SafeURL("/v/" + videoID + "/status") } -// tapirFrame1..3 are the ASCII tapir frames cycled by the TapirSpinner CSS -// animation. Three near-identical frames (snout and feet shift) read as a stocky, -// big-snouted tapir ambling in place while a summary is generated. +// Charmbracelet-inspired palette for the summarizing animation (TapirSpinner) — +// a charm purple box, pink tapir, mint snout/eyes/progress. Kept as named consts +// so the inline span colours and the CSS track/fill share one source of truth. const ( - tapirFrame1 = ` /\_____/\ -( · ω · ) - ) ∩∩ ( -(__( )__)` - tapirFrame2 = ` /\_____/\ -( · ω · ) - ) ∩∩ ( - (__( ^ )__)` - tapirFrame3 = ` /\_____/\ -( · ω · ) - ) ∪∪ ( -(__( )__)` + CharmPurple = "#7653FC" // box border + CharmPink = "#FF6E9C" // tapir body + CharmMint = "#0EF9B6" // snout, eyes, progress fill + CharmCream = "#FFFDF5" // bright text + CharmDim = "#6C6C6C" // dim text + charmTrack = "#2D2D2D" // empty progress track (internal: dark char colour) +) + +// tapirInteriorW is the fixed inner width of the Charm box, in monospace cells. +const tapirInteriorW = 34 + +// tapirBarFill is the mint progress fill (27 cells), revealed left→right by the +// CSS width/clip animation over the dim track drawn in each frame. +const tapirBarFill = "███████████████████████████" + +// tapirRun is one coloured (or uncoloured) text segment of a box row. +type tapirRun struct { + s string + color string // "" = no span (plain text) +} + +func tapirSpan(color, s string) string { + if color == "" { + return s + } + return `` + s + `` +} + +// tapirLine renders one interior box row: concatenate the coloured runs, pad with +// spaces to the fixed interior width, then flank with the purple side borders. +// Padding is computed from the runs' rune counts, so every row's right border +// lines up no matter how many runs it has (assuming 1-cell monospace glyphs). +func tapirLine(runs ...tapirRun) string { + var b strings.Builder + width := 0 + for _, r := range runs { + b.WriteString(tapirSpan(r.color, r.s)) + width += utf8.RuneCountInString(r.s) + } + if width < tapirInteriorW { + b.WriteString(strings.Repeat(" ", tapirInteriorW-width)) + } + bar := tapirSpan(CharmPurple, "│") + return bar + b.String() + bar +} + +// tapirFrameHTML builds one animation frame: a rounded Charm box around a colored +// ASCII tapir, a dim progress track, and labels. snout is the wiggling nose glyph +// that differs between the three frames. Returned as raw HTML (coloured spans), +// emitted verbatim by the template via templ.Raw. +func tapirFrameHTML(snout string) string { + top := tapirSpan(CharmPurple, "╭"+strings.Repeat("─", tapirInteriorW)+"╮") + bottom := tapirSpan(CharmPurple, "╰"+strings.Repeat("─", tapirInteriorW)+"╯") + lines := []string{ + top, + tapirLine(tapirRun{s: " "}, tapirRun{s: "◆", color: CharmMint}, tapirRun{s: " "}, tapirRun{s: "tapir", color: CharmCream}), + tapirLine(), + tapirLine(tapirRun{s: " "}, tapirRun{s: "▄▄▄▄▄", color: CharmPink}), + tapirLine(tapirRun{s: " "}, tapirRun{s: "▄█▓▓▓▓█▄", color: CharmPink}, tapirRun{s: " "}, tapirRun{s: snout, color: CharmMint}), + tapirLine(tapirRun{s: " "}, tapirRun{s: "█▓(", color: CharmPink}, tapirRun{s: " "}, tapirRun{s: "◕ ◕", color: CharmMint}, tapirRun{s: ")▓█", color: CharmPink}, tapirRun{s: "──┘", color: CharmMint}, tapirRun{s: " "}, tapirRun{s: "< thinking...", color: CharmDim}), + tapirLine(tapirRun{s: " "}, tapirRun{s: "▀█▓▓▓▓█▀", color: CharmPink}), + tapirLine(tapirRun{s: " "}, tapirRun{s: "██▄▄██", color: CharmPink}), + tapirLine(tapirRun{s: " "}, tapirRun{s: "▀▀", color: CharmPink}, tapirRun{s: " "}, tapirRun{s: "▀▀", color: CharmPink}), + tapirLine(), + tapirLine(tapirRun{s: " "}, tapirRun{s: "[", color: CharmDim}, tapirRun{s: strings.Repeat("░", 27), color: charmTrack}, tapirRun{s: "]", color: CharmDim}), + tapirLine(tapirRun{s: " "}, tapirRun{s: "summarizing", color: CharmDim}), + bottom, + } + return strings.Join(lines, "\n") +} + +// The three frames differ only in the snout glyph (∩ → ∪ → ~), cross-faded by CSS +// to read as a tapir wiggling its nose while it thinks. +var ( + tapirFrameHTML1 = tapirFrameHTML("∩") + tapirFrameHTML2 = tapirFrameHTML("∪") + tapirFrameHTML3 = tapirFrameHTML("~") ) // summarizeModeLabel names the current mode for display. @@ -405,20 +471,29 @@ main { max-width: 60rem; margin: 0 auto; padding: var(--s4) var(--s3); } .card-pending { border-style: dashed; } .card-pending .card-title { color: var(--muted); font-weight: 600; } -/* summarizing animation — a little ASCII tapir ambling in place. Three frames - are stacked absolutely and cross-faded by a stepped keyframe with staggered - delays, so exactly one shows at a time (no JS). */ +/* summarizing animation — a Charmbracelet-style TUI panel rendered in the + browser: a dark terminal card, a rounded purple box around a pink ASCII tapir, + and a lipgloss-style progress bar. Three frames are stacked and cross-faded by + a stepped keyframe (staggered delays) so the snout appears to wiggle; the + progress fill grows independently via a clip animation over the dim track. */ .card-processing { border-style: dashed; } -.tapir-spin { position: relative; height: 5.2em; margin: var(--s2) 0; } -.tapir-spin pre { position: absolute; inset: 0; margin: 0; opacity: 0; font: .9rem/1.15 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--accent); animation: tapir-cycle 1.2s steps(1, end) infinite; } -.tapir-spin pre:nth-child(1) { animation-delay: 0s; } -.tapir-spin pre:nth-child(2) { animation-delay: .4s; } -.tapir-spin pre:nth-child(3) { animation-delay: .8s; } +.tapir-charm { position: relative; display: inline-block; background: #0d0d12; border-radius: 10px; padding: .8em 1em; margin: var(--s2) 0; font: .82rem/1.15 ui-monospace, SFMono-Regular, Menlo, "Cascadia Code", monospace; box-shadow: 0 2px 14px rgba(118, 83, 252, .25); } +.tapir-charm pre { margin: 0; white-space: pre; opacity: 0; animation: tapir-cycle 1.2s steps(1, end) infinite; } +.tapir-charm .tapir-f1 { position: relative; animation-delay: 0s; } +.tapir-charm .tapir-f2 { position: absolute; top: .8em; left: 1em; animation-delay: .4s; } +.tapir-charm .tapir-f3 { position: absolute; top: .8em; left: 1em; animation-delay: .8s; } @keyframes tapir-cycle { 0%, 33.32% { opacity: 1; } 33.33%, 100% { opacity: 0; } } +/* progress fill: 27 mint cells overlaying the dim track at box row 10, col 3, + revealed left→right over 8s, looping. */ +.tapir-bar { position: absolute; top: calc(.8em + 11.5em); left: calc(1em + 3ch); height: 1.15em; line-height: 1.15; overflow: hidden; } +.tapir-bar-fill { animation: tapir-fill 8s linear infinite; text-shadow: 0 0 6px rgba(14, 249, 182, .7); } +@keyframes tapir-fill { 0% { clip-path: inset(0 100% 0 0); } 100% { clip-path: inset(0 0 0 0); } } .tapir-label { color: var(--muted); font-size: .9rem; margin: 0; } @media (prefers-reduced-motion: reduce) { - .tapir-spin pre { animation: none; } - .tapir-spin pre:nth-child(1) { opacity: 1; } + .tapir-charm pre { animation: none; } + .tapir-charm .tapir-f2, .tapir-charm .tapir-f3 { display: none; } + .tapir-charm .tapir-f1 { opacity: 1; } + .tapir-bar-fill { animation: none; clip-path: inset(0 35% 0 0); } } /* summarization mode toggle on the account page */ diff --git a/internal/web/view_internal_test.go b/internal/web/view_internal_test.go index 1e2d1b6..30e51d5 100644 --- a/internal/web/view_internal_test.go +++ b/internal/web/view_internal_test.go @@ -1,6 +1,11 @@ package web -import "testing" +import ( + "regexp" + "strings" + "testing" + "unicode/utf8" +) func TestEmbedURL(t *testing.T) { tests := []struct { @@ -27,3 +32,19 @@ func TestEmbedURL(t *testing.T) { }) } } + +// TestTapirFrameRowsAligned asserts every box row has the same cell width once +// the inline-colour spans are stripped, so the rounded border lines up on every +// line (the panel only looks right if the right │ is flush across all rows). +func TestTapirFrameRowsAligned(t *testing.T) { + stripSpan := regexp.MustCompile(`]*>`) + for name, frame := range map[string]string{"f1": tapirFrameHTML1, "f2": tapirFrameHTML2, "f3": tapirFrameHTML3} { + plain := stripSpan.ReplaceAllString(frame, "") + want := tapirInteriorW + 2 // both purple side borders + for i, line := range strings.Split(plain, "\n") { + if got := utf8.RuneCountInString(line); got != want { + t.Errorf("%s line %d width = %d, want %d: %q", name, i, got, want, line) + } + } + } +} diff --git a/internal/web/views.templ b/internal/web/views.templ index 7ce9ad9..dc526d6 100644 --- a/internal/web/views.templ +++ b/internal/web/views.templ @@ -140,14 +140,17 @@ templ VideoCard(r store.SummaryRow) { } -// TapirSpinner is the summarizing animation: three ASCII tapir frames stacked -// and cross-faded by CSS keyframes (no JS), with a muted "Summarizing…" label. -// The art is aria-hidden — it is decorative; the label carries the meaning. +// TapirSpinner is the summarizing animation: a Charmbracelet-style TUI panel — +// three richly coloured ASCII tapir frames (inline span colours, snout wiggling +// ∩→∪→~) cross-faded by CSS, plus a lipgloss-style progress bar whose mint fill +// grows over the dim track. The panel is aria-hidden (decorative); the +// "Summarizing…" label below carries the meaning for assistive tech. templ TapirSpinner() { - ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if r.URL != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "

watch on source ↗

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\" rel=\"noopener noreferrer\">watch on source ↗

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -902,88 +915,88 @@ func DetailPage(r store.SummaryRow) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "

Summary

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

Summary

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var44 string - templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(r.Summary) + var templ_7745c5c3_Var43 string + templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(r.Summary) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 208, Col: 31} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 211, Col: 31} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(r.Highlights) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "

Highlights

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

    Highlights

      ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, h := range r.Highlights { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "
    • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "
    • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var45 string - templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(h) + var templ_7745c5c3_Var44 string + templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(h) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 215, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 218, Col: 14} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "
    • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if len(r.Takeaways) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "

Takeaways

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

    Takeaways

      ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, t := range r.Takeaways { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "
    • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "
    • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var46 string - templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(t) + var templ_7745c5c3_Var45 string + templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(t) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 225, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 228, Col: 14} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "
    • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = Layout("Tapir — "+displayTitle(r)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var38), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout("Tapir — "+displayTitle(r)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var37), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1010,12 +1023,12 @@ func RegisterPage(email, errMsg string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var47 := templ.GetChildren(ctx) - if templ_7745c5c3_Var47 == nil { - templ_7745c5c3_Var47 = templ.NopComponent + templ_7745c5c3_Var46 := templ.GetChildren(ctx) + if templ_7745c5c3_Var46 == nil { + templ_7745c5c3_Var46 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var48 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var47 := 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 { @@ -1027,59 +1040,59 @@ func RegisterPage(email, errMsg string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "

Complete your registration

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

Complete your registration

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if email != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "

Signed in as ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "

Signed in as ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var48 string + templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(email) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 245, Col: 40} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, ".

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "

Choose a display name to finish setting up your Tapir account.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if errMsg != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var49 string - templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(email) + templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 242, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 249, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, ".

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "

Choose a display name to finish setting up your Tapir account.

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if errMsg != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var50 string - templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 246, Col: 42} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = Layout("Tapir — Register").Render(templ.WithChildren(ctx, templ_7745c5c3_Var48), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout("Tapir — Register").Render(templ.WithChildren(ctx, templ_7745c5c3_Var47), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1107,12 +1120,12 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var51 := templ.GetChildren(ctx) - if templ_7745c5c3_Var51 == nil { - templ_7745c5c3_Var51 = templ.NopComponent + templ_7745c5c3_Var50 := templ.GetChildren(ctx) + if templ_7745c5c3_Var50 == nil { + templ_7745c5c3_Var50 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var52 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var51 := 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 { @@ -1128,43 +1141,43 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "

Account

Display name
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "

Account

Display name
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var53 string - templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(displayNameOr(displayName)) + var templ_7745c5c3_Var52 string + templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(displayNameOr(displayName)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 274, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 277, Col: 36} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if email != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "
Signed in as
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "
Signed in as
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var54 string - templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(email) + var templ_7745c5c3_Var53 string + templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(email) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 277, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 280, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "

Summarization

Automatic summarizes every new video as it is discovered. Manual lets you pick which videos to summarize — new videos appear in your list with a Summarize button.

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

Summarization

Automatic summarizes every new video as it is discovered. Manual lets you pick which videos to summarize — new videos appear in your list with a Summarize button.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1172,119 +1185,119 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "

Connected accounts

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

Connected accounts

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(conns) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "

No connected video accounts yet.

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

No connected video accounts yet.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "
      ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, c := range conns { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "
    • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "
    • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var55 string - templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(providerLabel(c.Provider)) + var templ_7745c5c3_Var54 string + templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(providerLabel(c.Provider)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 298, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 301, Col: 64} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if c.ProviderAccount != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var56 string - templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(c.ProviderAccount) + var templ_7745c5c3_Var55 string + templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(c.ProviderAccount) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 300, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 303, Col: 49} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var56 string + templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(c.Status) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 305, Col: 38} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "
      connected ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var57 string - templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(c.Status) + templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(c.ConnectedAt.Format("2006-01-02")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 302, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 307, Col: 83} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "
      connected ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "
    • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if !hasYouTube(conns) { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "

Connect YouTube

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

Connect YouTube

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "

Delete account

Permanently remove your Tapir account and all of its data — summaries, watch/skip/save actions, and connected accounts. This cannot be undone.

Delete account…

This permanently deletes your account and all data. Are you sure?

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

Delete account

Permanently remove your Tapir account and all of its data — summaries, watch/skip/save actions, and connected accounts. This cannot be undone.

Delete account…

This permanently deletes your account and all data. Are you sure?

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = Layout("Tapir — Account").Render(templ.WithChildren(ctx, templ_7745c5c3_Var52), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout("Tapir — Account").Render(templ.WithChildren(ctx, templ_7745c5c3_Var51), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1312,51 +1325,51 @@ func summarizeModeControl(auto bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var60 := templ.GetChildren(ctx) - if templ_7745c5c3_Var60 == nil { - templ_7745c5c3_Var60 = templ.NopComponent + templ_7745c5c3_Var59 := templ.GetChildren(ctx) + if templ_7745c5c3_Var59 == nil { + templ_7745c5c3_Var59 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "

Current mode: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "

Current mode: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var60 string + templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(summarizeModeLabel(auto)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 345, Col: 53} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1384,117 +1397,117 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var64 := templ.GetChildren(ctx) - if templ_7745c5c3_Var64 == nil { - templ_7745c5c3_Var64 = templ.NopComponent + templ_7745c5c3_Var63 := templ.GetChildren(ctx) + if templ_7745c5c3_Var63 == nil { + templ_7745c5c3_Var63 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\" hx-target=\"#action-buttons\" hx-swap=\"outerHTML\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, v := range actionVerbs { - var templ_7745c5c3_Var67 = []any{"action", templ.KV("active", active[v])} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var67...) + var templ_7745c5c3_Var66 = []any{"action", templ.KV("active", active[v])} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var66...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }