Paste view: collapsible stats pill under title (#17)
CI / test (push) Successful in 19s
CI / docker (push) Skipped

This commit is contained in:
2026-09-08 21:14:44 -05:00
parent 3facff3d1e
commit 7410b5c9cf
4 changed files with 54 additions and 10 deletions
BIN
View File
Binary file not shown.
+10
View File
@@ -102,11 +102,21 @@ func (a *apiServer) renderPaste(w http.ResponseWriter, row *PasteRow, justCreate
if row.ExpiresAt.Valid {
expIn = expiryString(row.ExpiresAt.Int64)
}
lang := row.Language.String
if lang == "" {
lang = "text"
}
summary := fmt.Sprintf("%s · %s · %d views · %s", lang, humanSize(len(row.Content)), row.ViewCount, agoString(row.CreatedAt))
data := map[string]any{
"Page": "paste",
"ID": row.ID,
"Title": row.Title.String,
"Language": row.Language.String,
"StatsSummary": summary,
"SizeHuman": humanSize(len(row.Content)),
"HasPassword": row.PasswordHash.Valid,
"BurnAfterRead": row.BurnAfterRead,
"CustomSlug": row.CustomSlug.String,
"ContentHTML": template.HTML(highlightCode(row.Content, row.Language.String)), // safe: highlightCode escapes all non-span text
"ContentAttr": row.Content,
"Gutter": strings.TrimSuffix(gutter, "\n"),
+12
View File
@@ -114,6 +114,18 @@ body {
.meta-bar h1 { font-size: 29.2px; font-weight: 600; }
.slug { font-family: var(--font-mono); font-size: 21.6px; color: var(--muted-fg); background: var(--surface-2); padding: 3px 9px; border-radius: var(--radius); }
.tag { font-size: 19.8px; color: var(--muted-fg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 2px 9px; }
.paste-title-bar { display: flex; align-items: center; gap: 12px; padding: 12px 18px; flex-wrap: wrap; }
.paste-title-bar h1 { font-size: 29.2px; font-weight: 600; margin: 0; }
.stats-pill { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.stats-head { display: flex; align-items: center; gap: 10px; width: 100%; background: none; border: 0; color: var(--muted-fg); font: inherit; font-size: 21.6px; padding: 10px 14px; cursor: pointer; text-align: left; }
.stats-head:hover { color: var(--fg); background: var(--surface-2); }
.stats-chev { width: 18px; height: 18px; flex: none; transition: transform 0.15s ease; }
.stats-pill.open .stats-chev { transform: rotate(180deg); }
.stats-summary { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stats-body { border-top: 1px solid var(--border); }
.stats-grid { display: grid; grid-template-columns: max-content 1fr; gap: 6px 18px; padding: 12px 16px; font-size: 20.7px; }
.stats-k { color: var(--muted-fg); }
.stats-v { color: var(--fg); word-break: break-all; }
.meta-bar .spacer { flex: 1; }
.iconbtn { border: 1px solid var(--border); background: var(--surface-2); color: var(--muted-fg); border-radius: var(--radius); padding: 5px 12px; font: inherit; font-size: 21.6px; cursor: pointer; text-decoration: none; }
.iconbtn:hover { color: var(--fg); border-color: var(--muted); }
+32 -10
View File
@@ -2,17 +2,36 @@
{{template "topbar" .}}
<div class="page">
<div class="float">
<div class="meta-bar">
<div class="paste-title-bar">
<h1>{{if .Title}}{{.Title}}{{else}}Untitled paste{{end}}</h1>
<span class="slug">/{{.ID}}</span>
{{if .Language}}<span class="tag">{{.Language}}</span>{{end}}
{{if .ExpiresAt}}<span class="tag">expires in {{.ExpiresIn}}</span>{{end}}
<span class="slug">/{{if .CustomSlug}}{{.CustomSlug}}{{else}}{{.ID}}{{end}}</span>
<div class="spacer"></div>
<a class="iconbtn" href="/raw/{{.ID}}">raw</a>
<a class="iconbtn" href="#" onclick="copyContent(); return false;">copy</a>
{{if .DeletionToken}}<a class="iconbtn danger" href="#" onclick="redeem('{{.DeletionToken}}'); return false;">delete</a>{{end}}
</div>
</div>
<div class="float">
<div class="stats-pill" id="stats-pill">
<button type="button" class="stats-head" id="stats-toggle" aria-expanded="false" onclick="toggleStats()">
<svg class="stats-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
<span class="stats-summary">{{.StatsSummary}}</span>
</button>
<div class="stats-body" id="stats-body" hidden>
<div class="stats-grid">
<span class="stats-k">Language</span><span class="stats-v">{{if .Language}}{{.Language}}{{else}}text{{end}}</span>
<span class="stats-k">Size</span><span class="stats-v">{{.SizeHuman}} ({{.LineCount}} lines)</span>
<span class="stats-k">Views</span><span class="stats-v">{{.ViewCount}}</span>
<span class="stats-k">Created</span><span class="stats-v">{{.CreatedAgo}}</span>
{{if .ExpiresAt}}<span class="stats-k">Expires</span><span class="stats-v">in {{.ExpiresIn}}</span>{{end}}
<span class="stats-k">Password</span><span class="stats-v">{{if .HasPassword}}protected{{else}}none{{end}}</span>
{{if .BurnAfterRead}}<span class="stats-k">Burn</span><span class="stats-v">burn after read</span>{{end}}
{{if .CustomSlug}}<span class="stats-k">Custom URL</span><span class="stats-v">/{{.CustomSlug}}</span>{{end}}
<span class="stats-k">Visibility</span><span class="stats-v">{{.Visibility}}</span>
</div>
</div>
</div>
</div>
{{if .JustCreated}}
<div class="float">
<div class="created-banner" style="display:block">
@@ -22,13 +41,7 @@
</div>
{{end}}
<div class="float">
<div class="code-head"><span class="dot"></span> {{.LineCount}} lines · {{.SizeBytes}} bytes</div>
<div class="code"><div class="gutter">{{.Gutter}}</div><div class="codebody" id="codebody">{{.ContentHTML}}</div></div>
<div class="footnote">
<span>Created {{.CreatedAgo}}</span>
<span>{{.ViewCount}} views</span>
<span>{{.Visibility}}</span>
</div>
</div>
</div>
<input type="hidden" id="raw-content" value="{{.ContentAttr}}">
@@ -41,6 +54,15 @@ function toast(msg) {
clearTimeout(t._h);
t._h = setTimeout(() => t.classList.remove('show'), 2000);
}
function toggleStats() {
const body = document.getElementById('stats-body');
const pill = document.getElementById('stats-pill');
const btn = document.getElementById('stats-toggle');
const open = body.hidden;
body.hidden = !open;
pill.classList.toggle('open', open);
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
}
function copyContent() {
navigator.clipboard.writeText(document.getElementById('raw-content').value);
toast('Copied');