forked from goffee/cup
471 lines
No EOL
16 KiB
HTML
471 lines
No EOL
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>{{.SiteTitle}} — template lab</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=JetBrains+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet" />
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
:root {
|
||
--bg: #0d0d0f;
|
||
--surface: #141416;
|
||
--border: #222226;
|
||
--muted: #3a3a40;
|
||
--subtle: #6b6b75;
|
||
--body: #c8c8d0;
|
||
--heading: #f0f0f4;
|
||
--accent: #e8c547;
|
||
--accent2: #5b8cff;
|
||
--danger: #ff6b6b;
|
||
--mono: 'JetBrains Mono', monospace;
|
||
--serif: 'Playfair Display', Georgia, serif;
|
||
--sans: 'DM Sans', sans-serif;
|
||
}
|
||
|
||
html { background: var(--bg); color: var(--body); font-family: var(--sans); font-size: 16px; line-height: 1.6; }
|
||
|
||
body { max-width: 1100px; margin: 0 auto; padding: 0 2rem 6rem; }
|
||
|
||
/* ── Header ── */
|
||
header {
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 3rem 0 2rem;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-end;
|
||
gap: 1rem;
|
||
}
|
||
.site-title {
|
||
font-family: var(--mono);
|
||
font-size: .75rem;
|
||
letter-spacing: .2em;
|
||
text-transform: uppercase;
|
||
color: var(--accent);
|
||
}
|
||
.site-headline {
|
||
font-family: var(--serif);
|
||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||
color: var(--heading);
|
||
line-height: 1.1;
|
||
margin-top: .4rem;
|
||
}
|
||
.site-headline em { color: var(--accent); font-style: italic; }
|
||
.helper-count {
|
||
font-family: var(--mono);
|
||
font-size: .7rem;
|
||
color: var(--subtle);
|
||
text-align: right;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* ── Section label ── */
|
||
.section-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
margin: 4rem 0 1.5rem;
|
||
}
|
||
.section-label span {
|
||
font-family: var(--mono);
|
||
font-size: .65rem;
|
||
letter-spacing: .15em;
|
||
text-transform: uppercase;
|
||
color: var(--accent);
|
||
white-space: nowrap;
|
||
}
|
||
.section-label::after {
|
||
content: '';
|
||
flex: 1;
|
||
height: 1px;
|
||
background: var(--border);
|
||
}
|
||
|
||
/* ── Helper badge ── */
|
||
.badge {
|
||
display: inline-block;
|
||
font-family: var(--mono);
|
||
font-size: .6rem;
|
||
letter-spacing: .08em;
|
||
padding: .15em .5em;
|
||
border-radius: 3px;
|
||
border: 1px solid var(--muted);
|
||
color: var(--subtle);
|
||
vertical-align: middle;
|
||
margin-left: .4rem;
|
||
}
|
||
.badge-accent { border-color: var(--accent); color: var(--accent); }
|
||
.badge-blue { border-color: var(--accent2); color: var(--accent2); }
|
||
|
||
/* ── Demo block ── */
|
||
.demo-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||
gap: 1px;
|
||
background: var(--border);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
.demo-cell {
|
||
background: var(--surface);
|
||
padding: 1.5rem;
|
||
}
|
||
.demo-cell h3 {
|
||
font-family: var(--mono);
|
||
font-size: .7rem;
|
||
letter-spacing: .1em;
|
||
text-transform: uppercase;
|
||
color: var(--accent);
|
||
margin-bottom: .75rem;
|
||
}
|
||
.demo-cell .result {
|
||
font-size: 1rem;
|
||
color: var(--heading);
|
||
}
|
||
.demo-cell .note {
|
||
margin-top: .5rem;
|
||
font-size: .75rem;
|
||
color: var(--subtle);
|
||
font-family: var(--mono);
|
||
}
|
||
|
||
/* ── Article cards ── */
|
||
.article-list { display: flex; flex-direction: column; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
||
|
||
.article-card { background: var(--surface); padding: 2rem; display: grid; grid-template-columns: 1fr auto; gap: 1rem; align-items: start; transition: background .15s; }
|
||
.article-card:hover { background: #18181c; }
|
||
|
||
.card-meta { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; margin-bottom: .75rem; }
|
||
.tag {
|
||
font-family: var(--mono);
|
||
font-size: .6rem;
|
||
letter-spacing: .08em;
|
||
text-transform: uppercase;
|
||
padding: .2em .55em;
|
||
background: var(--muted);
|
||
border-radius: 3px;
|
||
color: var(--body);
|
||
}
|
||
.tag-featured { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); }
|
||
|
||
.card-title { font-family: var(--serif); font-size: 1.35rem; color: var(--heading); line-height: 1.25; margin-bottom: .5rem; }
|
||
.card-subtitle { font-size: .85rem; color: var(--accent2); margin-bottom: .6rem; font-style: italic; }
|
||
.card-excerpt { font-size: .875rem; color: var(--body); line-height: 1.65; }
|
||
|
||
.card-author { display: flex; align-items: center; gap: .6rem; margin-top: 1.25rem; }
|
||
.avatar {
|
||
width: 32px; height: 32px; border-radius: 50%;
|
||
background: var(--muted);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-family: var(--mono); font-size: .55rem; color: var(--accent);
|
||
flex-shrink: 0;
|
||
}
|
||
.author-name { font-size: .8rem; color: var(--heading); }
|
||
.author-date { font-size: .72rem; color: var(--subtle); font-family: var(--mono); }
|
||
|
||
.card-aside { text-align: right; }
|
||
.views { font-family: var(--mono); font-size: .75rem; color: var(--subtle); white-space: nowrap; }
|
||
.views strong { color: var(--body); display: block; font-size: 1rem; }
|
||
.price-badge {
|
||
display: inline-block; margin-top: .75rem;
|
||
font-family: var(--mono); font-size: .7rem;
|
||
padding: .3em .7em; border-radius: 4px;
|
||
background: color-mix(in srgb, var(--accent2) 15%, transparent);
|
||
color: var(--accent2); border: 1px solid var(--accent2);
|
||
}
|
||
.price-free {
|
||
background: color-mix(in srgb, #4caf50 12%, transparent);
|
||
color: #6fcf97; border-color: #4caf50;
|
||
}
|
||
|
||
/* ── Logic demo table ── */
|
||
.logic-table { width: 100%; border-collapse: collapse; font-size: .85rem; }
|
||
.logic-table th {
|
||
font-family: var(--mono); font-size: .65rem; letter-spacing: .1em; text-transform: uppercase;
|
||
color: var(--subtle); text-align: left; padding: .6rem 1rem;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.logic-table td { padding: .75rem 1rem; border-bottom: 1px solid var(--border); color: var(--body); }
|
||
.logic-table tr:last-child td { border-bottom: none; }
|
||
.logic-table tr:hover td { background: var(--surface); }
|
||
.val { font-family: var(--mono); font-size: .8rem; color: var(--accent); }
|
||
|
||
/* ── Footer ── */
|
||
footer {
|
||
margin-top: 5rem; padding-top: 2rem; border-top: 1px solid var(--border);
|
||
font-family: var(--mono); font-size: .65rem; color: var(--muted);
|
||
display: flex; justify-content: space-between; flex-wrap: wrap; gap: .5rem;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ════════════════════════════════════════════
|
||
HEADER
|
||
════════════════════════════════════════════ -->
|
||
<header>
|
||
<div>
|
||
<p class="site-title">{{.SiteTitle}}</p>
|
||
<h1 class="site-headline">Template <em>helpers</em><br>in action</h1>
|
||
</div>
|
||
<p class="helper-count">22 helpers registered<br>across 6 groups</p>
|
||
</header>
|
||
|
||
|
||
<!-- ════════════════════════════════════════════
|
||
STRING HELPERS
|
||
════════════════════════════════════════════ -->
|
||
<div class="section-label"><span>String helpers</span></div>
|
||
|
||
{{$first := first .Articles}}
|
||
|
||
<div class="demo-grid">
|
||
|
||
<div class="demo-cell">
|
||
<h3>capitalize <span class="badge">capitalize</span></h3>
|
||
<p class="result">{{capitalize $first.Title}}</p>
|
||
<p class="note">input: "{{$first.Title}}"</p>
|
||
</div>
|
||
|
||
<div class="demo-cell">
|
||
<h3>truncate <span class="badge">truncate</span></h3>
|
||
<p class="result">{{$first.Excerpt | truncate 80}}</p>
|
||
<p class="note">capped at 80 chars</p>
|
||
</div>
|
||
|
||
<div class="demo-cell">
|
||
<h3>prepend <span class="badge">prepend</span></h3>
|
||
<p class="result">{{prepend $first.Slug "/articles/"}}</p>
|
||
<p class="note">prepended "/articles/" to slug</p>
|
||
</div>
|
||
|
||
<div class="demo-cell">
|
||
<h3>strAppend <span class="badge">strAppend</span></h3>
|
||
<p class="result">{{strAppend $first.Slug ".html"}}</p>
|
||
<p class="note">appended ".html" to slug</p>
|
||
</div>
|
||
|
||
<div class="demo-cell">
|
||
<h3>split → join <span class="badge">split</span> <span class="badge">join</span></h3>
|
||
{{$parts := split "go,templates,funcmap" ","}}
|
||
<p class="result">{{join $parts " · "}}</p>
|
||
<p class="note">split on "," then joined with " · "</p>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
<!-- ════════════════════════════════════════════
|
||
NUMBER & DATE HELPERS
|
||
════════════════════════════════════════════ -->
|
||
<div class="section-label"><span>Number & Date helpers</span></div>
|
||
|
||
<div class="demo-grid">
|
||
|
||
<div class="demo-cell">
|
||
<h3>fmtNumber — int <span class="badge">fmtNumber</span></h3>
|
||
<p class="result">{{fmtNumber $first.Views}}</p>
|
||
<p class="note">raw value: {{$first.Views}}</p>
|
||
</div>
|
||
|
||
{{$paid := index .Articles 2}}
|
||
<div class="demo-cell">
|
||
<h3>fmtNumber — float <span class="badge">fmtNumber</span></h3>
|
||
<p class="result">$ {{fmtNumber $paid.Price}}</p>
|
||
<p class="note">raw value: {{$paid.Price}}</p>
|
||
</div>
|
||
|
||
<div class="demo-cell">
|
||
<h3>fmtDate "short" <span class="badge">fmtDate</span></h3>
|
||
<p class="result">{{fmtDate $first.PublishedAt "short"}}</p>
|
||
<p class="note">layout: "02 Jan 2006"</p>
|
||
</div>
|
||
|
||
<div class="demo-cell">
|
||
<h3>fmtDate "long" <span class="badge">fmtDate</span></h3>
|
||
<p class="result">{{fmtDate $first.PublishedAt "long"}}</p>
|
||
<p class="note">layout: "02 January 2006"</p>
|
||
</div>
|
||
|
||
<div class="demo-cell">
|
||
<h3>fmtDate "iso" <span class="badge">fmtDate</span></h3>
|
||
<p class="result">{{fmtDate $first.PublishedAt "iso"}}</p>
|
||
<p class="note">layout: "2006-01-02"</p>
|
||
</div>
|
||
|
||
<div class="demo-cell">
|
||
<h3>timeAgo <span class="badge">timeAgo</span></h3>
|
||
{{range .Articles}}
|
||
<p class="result" style="margin-bottom:.3rem">{{timeAgo .PublishedAt}} <span class="note" style="display:inline">— {{fmtDate .PublishedAt "short"}}</span></p>
|
||
{{end}}
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
<!-- ════════════════════════════════════════════
|
||
COLLECTION HELPERS
|
||
════════════════════════════════════════════ -->
|
||
<div class="section-label"><span>Collection helpers</span></div>
|
||
|
||
<div class="demo-grid">
|
||
|
||
<div class="demo-cell">
|
||
<h3>first <span class="badge">first</span></h3>
|
||
{{with first .Articles}}
|
||
<p class="result">{{capitalize .Title}}</p>
|
||
<p class="note">first article in the list</p>
|
||
{{end}}
|
||
</div>
|
||
|
||
<div class="demo-cell">
|
||
<h3>last <span class="badge">last</span></h3>
|
||
{{with last .Articles}}
|
||
<p class="result">{{capitalize .Title}}</p>
|
||
<p class="note">last article in the list</p>
|
||
{{end}}
|
||
</div>
|
||
|
||
<div class="demo-cell">
|
||
<h3>sliceOf 0–2 <span class="badge">sliceOf</span></h3>
|
||
{{range sliceOf .Articles 0 2}}
|
||
<p class="result" style="margin-bottom:.25rem">— {{capitalize .Title}}</p>
|
||
{{end}}
|
||
<p class="note">only first 2 of {{len .Articles}} articles</p>
|
||
</div>
|
||
|
||
<div class="demo-cell">
|
||
<h3>contains — slice <span class="badge">contains</span></h3>
|
||
{{if contains $first.Tags "go"}}
|
||
<p class="result" style="color:var(--accent)">✓ has tag "go"</p>
|
||
{{else}}
|
||
<p class="result" style="color:var(--danger)">✗ missing tag "go"</p>
|
||
{{end}}
|
||
<p class="note">tags: {{join $first.Tags ", "}}</p>
|
||
</div>
|
||
|
||
<div class="demo-cell">
|
||
<h3>contains — string <span class="badge">contains</span></h3>
|
||
{{if contains $first.Excerpt "FuncMap"}}
|
||
<p class="result" style="color:var(--accent)">✓ excerpt mentions "FuncMap"</p>
|
||
{{else}}
|
||
<p class="result" style="color:var(--danger)">✗ not found</p>
|
||
{{end}}
|
||
<p class="note">substring search on .Excerpt</p>
|
||
</div>
|
||
|
||
<div class="demo-cell">
|
||
<h3>join <span class="badge">join</span></h3>
|
||
<p class="result">{{join $first.Tags " / "}}</p>
|
||
<p class="note">tags joined with " / "</p>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
<!-- ════════════════════════════════════════════
|
||
LOGIC HELPERS
|
||
════════════════════════════════════════════ -->
|
||
<div class="section-label"><span>Logic helpers</span></div>
|
||
|
||
<table class="logic-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Helper</th>
|
||
<th>Article</th>
|
||
<th>Input</th>
|
||
<th>Output</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{{range .Articles}}
|
||
<tr>
|
||
<td><span class="badge badge-accent">defaultVal</span></td>
|
||
<td>{{.Title | capitalize | truncate 30}}</td>
|
||
<td class="val">.Subtitle ({{if .Subtitle}}"{{.Subtitle}}"{{else}}empty{{end}})</td>
|
||
<td class="val">{{defaultVal "No subtitle" .Subtitle}}</td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="badge badge-blue">ternary</span></td>
|
||
<td>{{capitalize .Title | truncate 30}}</td>
|
||
<td class="val">.Featured = {{.Featured}}</td>
|
||
<td class="val">{{ternary "⭐ featured" "regular" .Featured}}</td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="badge badge-accent">coalesce</span></td>
|
||
<td>{{capitalize .Title | truncate 30}}</td>
|
||
<td class="val">.Subtitle → .Title</td>
|
||
<td class="val">{{coalesce .Subtitle .Title}}</td>
|
||
</tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
|
||
|
||
<!-- ════════════════════════════════════════════
|
||
FULL ARTICLE CARDS (all helpers combined)
|
||
════════════════════════════════════════════ -->
|
||
<div class="section-label"><span>Full article cards — all helpers combined</span></div>
|
||
|
||
<div class="article-list">
|
||
{{range .Articles}}
|
||
<div class="article-card">
|
||
<div>
|
||
<div class="card-meta">
|
||
{{if .Featured}}<span class="tag tag-featured">featured</span>{{end}}
|
||
{{range .Tags}}<span class="tag">{{.}}</span>{{end}}
|
||
</div>
|
||
|
||
<h2 class="card-title">{{capitalize .Title}}</h2>
|
||
|
||
{{with coalesce .Subtitle ""}}
|
||
<p class="card-subtitle">{{.}}</p>
|
||
{{end}}
|
||
|
||
<p class="card-excerpt">{{.Excerpt | truncate 160}}</p>
|
||
|
||
<a style="font-family:var(--mono);font-size:.7rem;color:var(--accent2);text-decoration:none;margin-top:.75rem;display:inline-block;"
|
||
href="{{prepend .Slug "/articles/"}}">
|
||
{{prepend .Slug "/articles/"}} →
|
||
</a>
|
||
|
||
<div class="card-author">
|
||
<div class="avatar">{{.Author.Avatar}}</div>
|
||
<div>
|
||
<div class="author-name">{{capitalize .Author.Name}}</div>
|
||
<div class="author-date">
|
||
{{fmtDate .PublishedAt "short"}} · {{timeAgo .PublishedAt}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-aside">
|
||
<div class="views">
|
||
<strong>{{fmtNumber .Views}}</strong>
|
||
views
|
||
</div>
|
||
{{if gt .Price 0.0}}
|
||
<span class="price-badge">$ {{fmtNumber .Price}}</span>
|
||
{{else}}
|
||
<span class="price-badge price-free">free</span>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
|
||
<!-- ════════════════════════════════════════════
|
||
FOOTER
|
||
════════════════════════════════════════════ -->
|
||
<footer>
|
||
<span>{{.SiteTitle}} / template sandbox</span>
|
||
<span>{{len .Articles}} articles · {{fmtDate (first .Articles).PublishedAt "iso"}} → {{fmtDate (last .Articles).PublishedAt "iso"}}</span>
|
||
</footer>
|
||
|
||
</body>
|
||
</html> |