346 lines
10 KiB
Plaintext
346 lines
10 KiB
Plaintext
package templates
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"github.com/a-h/templ"
|
|
|
|
"gitea.lab.audasmedia.com.au/sam/ai-resume/internal/stage"
|
|
)
|
|
|
|
// Layout is the whole document. It renders the stage binding it is handed and
|
|
// never inspects which driver produced it.
|
|
templ Layout(v LayoutView) {
|
|
<!DOCTYPE html>
|
|
<html lang="en-AU">
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
<title>{ v.Title }</title>
|
|
<meta name="description" content={ v.Description }/>
|
|
<meta name="color-scheme" content="dark"/>
|
|
<link rel="stylesheet" href="/static/site.css"/>
|
|
<link rel="stylesheet" href="/static/chroma.css"/>
|
|
<script src="/static/htmx.min.js" defer></script>
|
|
if v.Stage.Mode == stage.ModeStream {
|
|
<script src="/static/sse.js" defer></script>
|
|
}
|
|
</head>
|
|
<body>
|
|
<a class="skip-link" href="#main">Skip to content</a>
|
|
@Nav(v.Nav, v.Area)
|
|
<div class="shell">
|
|
<main id="main" class="main">
|
|
@Breadcrumbs(v.Crumbs)
|
|
{ children... }
|
|
</main>
|
|
@Stage(StageView{Binding: v.Stage})
|
|
</div>
|
|
@Footer(v)
|
|
</body>
|
|
</html>
|
|
}
|
|
|
|
// Nav is the six-area navigation: a sidebar on desktop, a <details> disclosure
|
|
// below 900 px so it still works with JavaScript disabled.
|
|
templ Nav(items []NavItem, current string) {
|
|
<header class="topbar">
|
|
<a class="brand" href="/">
|
|
<span class="brand-mark">SR</span>
|
|
<span class="brand-text">
|
|
Sam Rolfe
|
|
<span class="brand-sub">AI · Platform · Edge</span>
|
|
</span>
|
|
</a>
|
|
@SearchForm("", "nav-search")
|
|
<details class="nav-disclosure">
|
|
<summary class="nav-toggle" aria-label="Sections">Sections</summary>
|
|
<nav class="nav" aria-label="Sections">
|
|
@navLinks(items, current)
|
|
@evidenceLink(current)
|
|
</nav>
|
|
</details>
|
|
<nav class="nav nav-rail" aria-label="Sections">
|
|
@navLinks(items, current)
|
|
@evidenceLink(current)
|
|
</nav>
|
|
</header>
|
|
}
|
|
|
|
templ navLinks(items []NavItem, current string) {
|
|
for _, it := range items {
|
|
<a href={ templ.SafeURL(it.Href) } class={ navClass(it) } aria-current={ currentAttr(it) }>
|
|
<span class="nav-gutter" aria-hidden="true">{ it.Gutter }</span>
|
|
<span class="nav-title">{ it.Title }</span>
|
|
</a>
|
|
}
|
|
}
|
|
|
|
templ evidenceLink(current string) {
|
|
<a href="/evidence/" class={ navClassFor(current == "evidence") }>
|
|
<span class="nav-gutter" aria-hidden="true">EV</span>
|
|
<span class="nav-title">Evidence index</span>
|
|
</a>
|
|
}
|
|
|
|
// Breadcrumbs shows where the reader is, and is a real link trail.
|
|
templ Breadcrumbs(crumbs []Crumb) {
|
|
if len(crumbs) > 0 {
|
|
<nav class="crumbs" aria-label="Breadcrumb">
|
|
<ol>
|
|
for i, c := range crumbs {
|
|
<li>
|
|
if i == len(crumbs)-1 {
|
|
<span aria-current="page">{ c.Label }</span>
|
|
} else {
|
|
<a href={ templ.SafeURL(c.Href) }>{ c.Label }</a>
|
|
<span class="crumb-sep" aria-hidden="true">/</span>
|
|
}
|
|
</li>
|
|
}
|
|
</ol>
|
|
</nav>
|
|
}
|
|
}
|
|
|
|
// SearchForm is a GET form: it works with JavaScript disabled and is upgraded by
|
|
// HTMX when scripts are available.
|
|
templ SearchForm(q string, class string) {
|
|
<form class={ "search", class } action="/search" method="get" role="search"
|
|
hx-get="/search" hx-target="#search-results" hx-swap="innerHTML" hx-push-url="true">
|
|
<label class="visually-hidden" for={ "q-" + class }>Search the site</label>
|
|
<input id={ "q-" + class } type="search" name="q" value={ q } placeholder="Search — try “zigbee”, “signage”, “routing”" autocomplete="off"/>
|
|
<button type="submit">Search</button>
|
|
</form>
|
|
}
|
|
|
|
// Stage is the presentation panel. Everything about how it is fed comes from
|
|
// the binding: route mode swaps in pre-rendered fragments, stream mode listens
|
|
// on an event stream. The markup is identical either way.
|
|
templ Stage(v StageView) {
|
|
<aside id="stage" class="stage" aria-label="Presentation panel">
|
|
<header class="stage-head">
|
|
<h2 class="stage-title">Presentation panel</h2>
|
|
<p class="stage-driver" data-driver={ string(v.Binding.Mode) }>{ driverLabel(v.Binding.Mode) }</p>
|
|
</header>
|
|
<div id={ stage.PanelID } class="stage-body" { v.Binding.Attributes()... }>
|
|
if v.Fragment != nil {
|
|
@FragmentView(*v.Fragment)
|
|
} else {
|
|
<div class="stage-empty">
|
|
<p class="stage-empty-title">{ v.Binding.Label }</p>
|
|
<p class="stage-hint">{ v.Binding.Hint }</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
<footer class="stage-foot">
|
|
<a href="/evidence/">Every artefact</a>
|
|
<a href="/stream/">Streaming panel</a>
|
|
</footer>
|
|
</aside>
|
|
}
|
|
|
|
// FragmentView renders one stage fragment. This is the only place that knows
|
|
// what a fragment kind looks like, and it is shared by the route driver (at
|
|
// build time) and the stream driver (at request time).
|
|
templ FragmentView(f stage.Fragment) {
|
|
<article class={ "frag frag-" + f.Kind }>
|
|
<header class="frag-head">
|
|
<p class="frag-kind">{ f.Kind }</p>
|
|
<h3 class="frag-title">{ f.Title }</h3>
|
|
if f.Subtitle != "" {
|
|
<p class="frag-sub">{ f.Subtitle }</p>
|
|
}
|
|
</header>
|
|
switch f.Kind {
|
|
case stage.KindText:
|
|
<div class="frag-text">{ f.Body }</div>
|
|
case stage.KindCode:
|
|
if f.Code != nil {
|
|
<figure class="code">
|
|
<figcaption>{ f.Code.Label }<span class="code-lang">{ f.Code.Language }</span></figcaption>
|
|
@f.Code.HTML
|
|
</figure>
|
|
}
|
|
case stage.KindDiagram, stage.KindImage:
|
|
<figure class="media">
|
|
<img src={ templ.SafeURL(f.Src) } alt={ f.Alt } loading="lazy"/>
|
|
if f.Subtitle != "" {
|
|
<figcaption>{ f.Subtitle }</figcaption>
|
|
}
|
|
</figure>
|
|
case stage.KindVideo:
|
|
<figure class="media">
|
|
<video controls preload="metadata" src={ templ.SafeURL(f.Src) }></video>
|
|
if f.Subtitle != "" {
|
|
<figcaption>{ f.Subtitle }</figcaption>
|
|
}
|
|
</figure>
|
|
case stage.KindLinks:
|
|
if len(f.Links) > 0 {
|
|
<ul class="frag-links">
|
|
for _, l := range f.Links {
|
|
<li>
|
|
<a href={ templ.SafeURL(l.Href) }>{ l.Label }</a>
|
|
if l.Note != "" {
|
|
<span class="frag-note">{ l.Note }</span>
|
|
}
|
|
</li>
|
|
}
|
|
</ul>
|
|
}
|
|
}
|
|
<footer class="frag-foot">
|
|
if f.Confidence != "" {
|
|
<span class={ ConfidenceClass(f.Confidence) }>{ f.Confidence }</span>
|
|
}
|
|
if f.DocURL != "" {
|
|
<a class="frag-doc" href={ templ.SafeURL(f.DocURL) }>{ f.DocTitle }</a>
|
|
}
|
|
</footer>
|
|
</article>
|
|
}
|
|
|
|
// EvidenceTable lists artefacts. Rows with HXGet load the artefact into the
|
|
// stage panel; every row is also a plain link, so this works with JavaScript
|
|
// disabled.
|
|
templ EvidenceTable(rows []EvidenceRow) {
|
|
<table class="evidence">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Evidence</th>
|
|
<th scope="col">Kind</th>
|
|
<th scope="col">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, r := range rows {
|
|
<tr>
|
|
<td>
|
|
if r.HXGet != "" {
|
|
<a href={ templ.SafeURL(r.Href) } hx-get={ r.HXGet } hx-target={ stage.PanelTarget() } hx-swap="innerHTML" hx-push-url="false">{ r.Label }</a>
|
|
} else {
|
|
<a href={ templ.SafeURL(r.Href) }>{ r.Label }</a>
|
|
}
|
|
</td>
|
|
<td><span class="kind">{ r.Kind }</span></td>
|
|
<td>
|
|
if r.Confidence != "" {
|
|
<span class={ ConfidenceClass(r.Confidence) }>{ r.Confidence }</span>
|
|
}
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
}
|
|
|
|
// PillarCard is one landing-page pillar: the claim, the numbers, and the way in.
|
|
templ PillarCard(c PillarCardView) {
|
|
<article class="pillar">
|
|
<p class="pillar-kicker">
|
|
<span class="pillar-gutter" aria-hidden="true">{ c.Area.Gutter }</span>
|
|
if c.Area.Role != "" {
|
|
<span>{ c.Area.Role }</span>
|
|
}
|
|
</p>
|
|
<h2 class="pillar-title">
|
|
<a href={ templ.SafeURL(c.Href) }>{ c.Title }</a>
|
|
</h2>
|
|
<p class="pillar-claim">{ c.Claim }</p>
|
|
if len(c.Metrics) > 0 {
|
|
<ul class="metrics">
|
|
for _, m := range c.Metrics {
|
|
<li class="chip">
|
|
<strong>{ m.Value }</strong>
|
|
<span>{ m.Label }</span>
|
|
</li>
|
|
}
|
|
</ul>
|
|
}
|
|
if len(c.Tech) > 0 {
|
|
<ul class="tech" aria-label="Technology">
|
|
for _, t := range c.Tech {
|
|
<li>{ t }</li>
|
|
}
|
|
</ul>
|
|
}
|
|
<p class="pillar-foot">
|
|
<a class="pillar-go" href={ templ.SafeURL(c.Href) }>
|
|
if c.Level == 1 {
|
|
<span>Start here</span>
|
|
} else {
|
|
<span>Read the capability page</span>
|
|
}
|
|
</a>
|
|
<span class="pillar-count">{ strconv.Itoa(c.Pages) } pages</span>
|
|
<span class={ ConfidenceClass(c.Confidence) }>{ c.Confidence }</span>
|
|
</p>
|
|
</article>
|
|
}
|
|
|
|
// Timeline renders the history area as a timeline.
|
|
templ Timeline(items []TimelineItem) {
|
|
<ol class="timeline">
|
|
for _, it := range items {
|
|
<li class="timeline-item">
|
|
<p class="timeline-period">{ it.Period }</p>
|
|
<div class="timeline-body">
|
|
<h3><a href={ templ.SafeURL(it.Href) }>{ it.Title }</a></h3>
|
|
<p>{ it.Claim }</p>
|
|
<span class={ ConfidenceClass(it.Confidence) }>{ it.Confidence }</span>
|
|
</div>
|
|
</li>
|
|
}
|
|
</ol>
|
|
}
|
|
|
|
// CodeBlock renders an already-highlighted code exhibit.
|
|
templ CodeBlock(c stage.Code) {
|
|
<figure class="code">
|
|
<figcaption>{ c.Label }<span class="code-lang">{ c.Language }</span></figcaption>
|
|
{ c.HTML }
|
|
</figure>
|
|
}
|
|
|
|
// Footer closes the page with the honesty notes a recruiter needs.
|
|
templ Footer(v LayoutView) {
|
|
<footer class="footer">
|
|
<p class="footer-line">Twenty-five years shipping software · Melbourne · remote · hybrid · office · travel</p>
|
|
<p class="footer-note">
|
|
Status labels are literal: <span class="conf conf-running">running</span> and
|
|
<span class="conf conf-delivered">delivered</span> are claims; <span class="conf conf-in-progress">in-progress</span> and
|
|
<span class="conf conf-abandoned">abandoned</span> are stated as such.
|
|
</p>
|
|
<p class="footer-links">
|
|
<a href="/evidence/">Evidence index</a>
|
|
<a href="/search">Search</a>
|
|
<a href="/stream/">Streaming panel</a>
|
|
<span class="footer-copy">© { strconv.Itoa(v.Year) } Sam Rolfe</span>
|
|
</p>
|
|
</footer>
|
|
}
|
|
|
|
func navClass(it NavItem) string { return navClassFor(it.Current) }
|
|
|
|
func navClassFor(current bool) string {
|
|
if current {
|
|
return "nav-link nav-current"
|
|
}
|
|
return "nav-link"
|
|
}
|
|
|
|
func currentAttr(it NavItem) string {
|
|
if it.Current {
|
|
return "page"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func driverLabel(m stage.Mode) string {
|
|
if m == stage.ModeStream {
|
|
return "stream driver · SSE"
|
|
}
|
|
return "route driver · pre-rendered"
|
|
}
|