WIP Phase 2: content loader, stage contract, search index, templ components
This commit is contained in:
345
internal/web/templates/components.templ
Normal file
345
internal/web/templates/components.templ
Normal file
@@ -0,0 +1,345 @@
|
||||
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"
|
||||
}
|
||||
258
internal/web/templates/pages.templ
Normal file
258
internal/web/templates/pages.templ
Normal file
@@ -0,0 +1,258 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
// HomePage is the landing page: the 90-second read.
|
||||
templ HomePage(v HomeView) {
|
||||
@Layout(v.Layout) {
|
||||
<section class="hero">
|
||||
<p class="hero-kicker">Melbourne · twenty-five years · AI · platform · edge</p>
|
||||
<h1 class="hero-title">{ v.Positioning }</h1>
|
||||
<p class="hero-support">{ v.Support }</p>
|
||||
<p class="hero-facts">
|
||||
<span>{ strconv.Itoa(v.Areas) } areas</span>
|
||||
<span>{ strconv.Itoa(v.Pages) } pages</span>
|
||||
<span>{ strconv.Itoa(v.Artifacts) } linked artefacts</span>
|
||||
<span>no page needs a model to render</span>
|
||||
</p>
|
||||
@SearchForm("", "hero-search")
|
||||
</section>
|
||||
|
||||
<section class="pillars" aria-label="Six areas">
|
||||
for _, c := range v.Cards {
|
||||
@PillarCard(c)
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="honesty" aria-labelledby="how-to-read">
|
||||
<h2 id="how-to-read">How to read this site</h2>
|
||||
<ul>
|
||||
<li><strong>Level 1</strong> is one screen per area: the claim, the numbers, the way in.</li>
|
||||
<li><strong>Level 2</strong> is what is actually run, with an evidence table and the trade-offs.</li>
|
||||
<li><strong>Level 3</strong> is the artefact: code, diagram, screenshot, log.</li>
|
||||
<li>Every claim carries a <strong>status</strong>, and finished, running, in-progress and abandoned are all shown as they are.</li>
|
||||
<li>Every artefact is at most three clicks from here, and the <a href="/evidence/">evidence index</a> lists all of them on one page.</li>
|
||||
</ul>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
// DocPage renders an L1, L2 or L3 document.
|
||||
templ DocPage(v DocView) {
|
||||
@Layout(v.Layout) {
|
||||
<article class="doc">
|
||||
<header class="doc-head">
|
||||
<p class="doc-kicker">{ v.AreaTitle } · { v.LevelLabel }</p>
|
||||
<h1 class="doc-title">{ v.Doc.Title }</h1>
|
||||
<p class="doc-claim">{ v.Doc.Claim }</p>
|
||||
<div class="doc-chips">
|
||||
<span class={ ConfidenceClass(v.Doc.Confidence) }>{ v.Doc.Confidence }</span>
|
||||
if v.Doc.Period != "" {
|
||||
<span class="chip chip-plain">{ v.Doc.Period }</span>
|
||||
}
|
||||
for _, m := range v.Metrics {
|
||||
<li class="chip">
|
||||
<strong>{ m.Value }</strong>
|
||||
<span>{ m.Label }</span>
|
||||
</li>
|
||||
}
|
||||
</div>
|
||||
if len(v.Tech) > 0 {
|
||||
<ul class="tech" aria-label="Technology">
|
||||
for _, t := range v.Tech {
|
||||
<li>{ t }</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</header>
|
||||
|
||||
<div class="prose">
|
||||
@v.Body
|
||||
</div>
|
||||
|
||||
if len(v.Evidence) > 0 {
|
||||
<section class="doc-evidence" aria-labelledby="evidence-heading">
|
||||
<h2 id="evidence-heading">Evidence for this page</h2>
|
||||
<p class="section-note">Select a row to load the artefact into the presentation panel. Each row is also a link, so this works without JavaScript.</p>
|
||||
@EvidenceTable(evidenceRows(v.Evidence))
|
||||
</section>
|
||||
}
|
||||
|
||||
if len(v.Timeline) > 0 {
|
||||
<section class="doc-timeline" aria-labelledby="timeline-heading">
|
||||
<h2 id="timeline-heading">In time order</h2>
|
||||
@Timeline(v.Timeline)
|
||||
</section>
|
||||
}
|
||||
|
||||
if len(v.SubPages) > 0 {
|
||||
<section class="doc-sub" aria-labelledby="sub-heading">
|
||||
<h2 id="sub-heading">{ subHeading(v.Doc.Level) }</h2>
|
||||
<ul class="drill-list">
|
||||
for _, s := range v.SubPages {
|
||||
<li>
|
||||
<a href={ templ.SafeURL(s.Href) }>{ s.Label }</a>
|
||||
<span class="drill-kind">{ s.Kind }</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
}
|
||||
|
||||
if len(v.Backlinks) > 0 {
|
||||
<section class="doc-back" aria-labelledby="back-heading">
|
||||
<h2 id="back-heading">Referenced from</h2>
|
||||
<ul class="drill-list">
|
||||
for _, b := range v.Backlinks {
|
||||
<li><a href={ templ.SafeURL(b.Href) }>{ b.Label }</a></li>
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
}
|
||||
|
||||
<footer class="doc-foot">
|
||||
<p>Status <span class={ ConfidenceClass(v.Doc.Confidence) }>{ v.Doc.Confidence }</span> — { ConfidenceNote(v.Doc.Confidence) }</p>
|
||||
<p>Last verified { v.Doc.LastVerified } · id <code>{ v.Doc.ID }</code> · { v.Doc.Path }</p>
|
||||
</footer>
|
||||
</article>
|
||||
}
|
||||
}
|
||||
|
||||
// EvidencePage is one page listing every artefact, grouped by area. It is the
|
||||
// shortest path from the landing page to any proof.
|
||||
templ EvidencePage(v EvidenceView) {
|
||||
@Layout(v.Layout) {
|
||||
<article class="doc">
|
||||
<header class="doc-head">
|
||||
<p class="doc-kicker">Index</p>
|
||||
<h1>Evidence index</h1>
|
||||
<p class="doc-claim">Every artefact this site claims, on one page. { strconv.Itoa(v.Total) } links, each one a file in the repository.</p>
|
||||
</header>
|
||||
for _, g := range v.Groups {
|
||||
<section class="evidence-group">
|
||||
<h2><a href={ templ.SafeURL(g.Href) }>{ g.AreaTitle }</a></h2>
|
||||
@EvidenceTable(g.Rows)
|
||||
</section>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
}
|
||||
|
||||
// SearchPage is the keyword search. The results block is swapped by HTMX on the
|
||||
// same page; with JavaScript off the form simply reloads the page.
|
||||
templ SearchPage(v SearchView) {
|
||||
@Layout(v.Layout) {
|
||||
<article class="doc">
|
||||
<header class="doc-head">
|
||||
<p class="doc-kicker">Keyword search</p>
|
||||
<h1>Search { strconv.Itoa(v.Indexed) } pages</h1>
|
||||
<p class="doc-claim">Full-text search over page titles and body text. Served from a local index — no model, no network call.</p>
|
||||
</header>
|
||||
@SearchForm(v.Query, "page-search")
|
||||
@SearchResults(v)
|
||||
</article>
|
||||
}
|
||||
}
|
||||
|
||||
// SearchResults is swapped into #search-results by HTMX.
|
||||
templ SearchResults(v SearchView) {
|
||||
<div id="search-results" aria-live="polite">
|
||||
if v.Query == "" {
|
||||
<p class="section-note">Try “zigbee”, “signage”, “routing”, “backups” or “guardrails”.</p>
|
||||
} else if v.Failed {
|
||||
<p class="search-empty">That query could not be run. Try fewer or simpler words.</p>
|
||||
} else if len(v.Results) == 0 {
|
||||
<p class="search-empty">No page matches <q>{ v.Query }</q>. The <a href="/evidence/">evidence index</a> lists everything by area.</p>
|
||||
} else {
|
||||
<p class="search-count">{ strconv.Itoa(len(v.Results)) } results for <q>{ v.Query }</q></p>
|
||||
<ol class="results">
|
||||
for _, r := range v.Results {
|
||||
<li class="result">
|
||||
<h2><a href={ templ.SafeURL(r.URL) }>{ r.Title }</a></h2>
|
||||
<p class="result-meta">
|
||||
<span class="kind">{ r.Area }</span>
|
||||
<span class="kind">L{ strconv.Itoa(r.Level) }</span>
|
||||
<span class={ ConfidenceClass(r.Confidence) }>{ r.Confidence }</span>
|
||||
</p>
|
||||
<p class="result-snippet">{ SafeSnippet(r.Snippet) }</p>
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
// StreamPage proves the panel can be re-pointed at a stream. The layout and the
|
||||
// panel markup are the ones every other page uses; only the binding differs.
|
||||
templ StreamPage(v StreamView) {
|
||||
@Layout(v.Layout) {
|
||||
<article class="doc">
|
||||
<header class="doc-head">
|
||||
<p class="doc-kicker">Design note · not a product feature</p>
|
||||
<h1>The panel takes content from a driver</h1>
|
||||
<p class="doc-claim">This page renders the same layout and the same panel as every other page. Its panel is bound to an event stream instead of pre-rendered files, and no layout code changed.</p>
|
||||
</header>
|
||||
<div class="prose">
|
||||
<p>The presentation panel is a content sink. In Mode A it takes pre-rendered fragments over
|
||||
HTMX: you click a row in an evidence table and one file is swapped in. On this page the same
|
||||
panel is bound to <code>/api/stage/stream</code> and receives { strconv.Itoa(v.Frames) } fragments
|
||||
over server-sent events, one after another.</p>
|
||||
<p>Fragments are pushed, not requested. Nothing about the panel — its markup, its fragment
|
||||
view, its empty state — knows the difference. That is the point: the Phase 5 console
|
||||
re-points this panel at retrieval output and the layout does not change.</p>
|
||||
<p class="section-note">This demonstration streams the artefacts already built for the site, with
|
||||
no model in the loop. It exists to prove the transport, not to pretend an AI is answering.</p>
|
||||
</div>
|
||||
if len(v.Fragment) > 0 {
|
||||
<section class="doc-sub">
|
||||
<h2>Fragments in this stream</h2>
|
||||
<ul class="drill-list">
|
||||
for _, f := range v.Fragment {
|
||||
<li>
|
||||
<a href={ templ.SafeURL(f.Href) } hx-get={ f.HXGet } hx-target="#stage-content" hx-swap="innerHTML">{ f.Label }</a>
|
||||
<span class="drill-kind">{ f.Kind }</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
}
|
||||
|
||||
// NotFoundPage is the 404.
|
||||
templ NotFoundPage(v NotFoundView) {
|
||||
@Layout(v.Layout) {
|
||||
<article class="doc">
|
||||
<header class="doc-head">
|
||||
<p class="doc-kicker">404</p>
|
||||
<h1>That page is not here</h1>
|
||||
<p class="doc-claim">The link may be from an older build. The six areas and the evidence index are the reliable ways in.</p>
|
||||
</header>
|
||||
<ul class="drill-list">
|
||||
<li><a href="/">Landing page</a></li>
|
||||
<li><a href="/evidence/">Evidence index</a></li>
|
||||
<li><a href="/search">Search</a></li>
|
||||
</ul>
|
||||
</article>
|
||||
}
|
||||
}
|
||||
|
||||
func evidenceRows(links []LinkView) []EvidenceRow {
|
||||
rows := make([]EvidenceRow, 0, len(links))
|
||||
for _, l := range links {
|
||||
rows = append(rows, EvidenceRow{Label: l.Label, Href: l.Href, HXGet: l.HXGet, Kind: l.Kind, Confidence: l.Confidence})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func subHeading(level int) string {
|
||||
if level == 1 {
|
||||
return "Drill-downs in this area"
|
||||
}
|
||||
return "Artefacts behind this page"
|
||||
}
|
||||
223
internal/web/templates/view.go
Normal file
223
internal/web/templates/view.go
Normal file
@@ -0,0 +1,223 @@
|
||||
// Package templates holds every templ component and the view models they read.
|
||||
// Layout never branches on the stage driver: it renders the binding it is given.
|
||||
package templates
|
||||
|
||||
import (
|
||||
stdhtml "html"
|
||||
"strings"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
|
||||
"gitea.lab.audasmedia.com.au/sam/ai-resume/internal/content"
|
||||
"gitea.lab.audasmedia.com.au/sam/ai-resume/internal/search"
|
||||
"gitea.lab.audasmedia.com.au/sam/ai-resume/internal/stage"
|
||||
)
|
||||
|
||||
// NavItem is one left-nav entry.
|
||||
type NavItem struct {
|
||||
Title string
|
||||
Href string
|
||||
Gutter string
|
||||
Current bool
|
||||
}
|
||||
|
||||
// Crumb is one breadcrumb.
|
||||
type Crumb struct {
|
||||
Label string
|
||||
Href string
|
||||
}
|
||||
|
||||
// LayoutView is everything the chrome needs.
|
||||
type LayoutView struct {
|
||||
Title string
|
||||
Description string
|
||||
Area string
|
||||
Nav []NavItem
|
||||
Crumbs []Crumb
|
||||
Stage stage.Binding
|
||||
SearchQuery string
|
||||
Year int
|
||||
SkipAI bool // true on Mode A pages, which never need a model
|
||||
}
|
||||
|
||||
// StageView wraps the binding so the Stage component has one argument.
|
||||
type StageView struct {
|
||||
Binding stage.Binding
|
||||
// Fragment, when set, is rendered into the panel at build time.
|
||||
Fragment *stage.Fragment
|
||||
}
|
||||
|
||||
// MetricView is one approved chip.
|
||||
type MetricView struct {
|
||||
Label string
|
||||
Value string
|
||||
}
|
||||
|
||||
// LinkView is a drill-down link. HXGet makes it a stage trigger; Href keeps it
|
||||
// a working link when JavaScript is off.
|
||||
type LinkView struct {
|
||||
Label string
|
||||
Href string
|
||||
HXGet string
|
||||
Kind string
|
||||
Note string
|
||||
Confidence string
|
||||
}
|
||||
|
||||
// EvidenceRow is one row of an EvidenceTable.
|
||||
type EvidenceRow struct {
|
||||
Label string
|
||||
Href string
|
||||
HXGet string
|
||||
Kind string
|
||||
Confidence string
|
||||
}
|
||||
|
||||
// EvidenceGroup groups rows by area on the evidence index.
|
||||
type EvidenceGroup struct {
|
||||
AreaTitle string
|
||||
Href string
|
||||
Rows []EvidenceRow
|
||||
}
|
||||
|
||||
// TimelineItem is one row of a timeline.
|
||||
type TimelineItem struct {
|
||||
Period string
|
||||
Title string
|
||||
Href string
|
||||
Confidence string
|
||||
Claim string
|
||||
}
|
||||
|
||||
// PillarCardView is one of the six landing cards.
|
||||
type PillarCardView struct {
|
||||
Area content.Area
|
||||
Title string
|
||||
Claim string
|
||||
Href string
|
||||
Metrics []MetricView
|
||||
Tech []string
|
||||
Confidence string
|
||||
Level int
|
||||
Pages int
|
||||
}
|
||||
|
||||
// Layout of each page kind.
|
||||
type (
|
||||
// HomeView is the landing page.
|
||||
HomeView struct {
|
||||
Layout LayoutView
|
||||
Positioning string
|
||||
Support string
|
||||
Location string
|
||||
Cards []PillarCardView
|
||||
Artifacts int
|
||||
Pages int
|
||||
Areas int
|
||||
}
|
||||
|
||||
// DocView is an L1, L2 or L3 content page.
|
||||
DocView struct {
|
||||
Layout LayoutView
|
||||
Doc *content.Document
|
||||
AreaTitle string
|
||||
Role string
|
||||
Body templ.Component
|
||||
Metrics []MetricView
|
||||
Tech []string
|
||||
Evidence []LinkView
|
||||
Backlinks []LinkView
|
||||
SubPages []LinkView
|
||||
Timeline []TimelineItem
|
||||
LevelLabel string
|
||||
}
|
||||
|
||||
// EvidenceView is the index of every artefact.
|
||||
EvidenceView struct {
|
||||
Layout LayoutView
|
||||
Groups []EvidenceGroup
|
||||
Total int
|
||||
}
|
||||
|
||||
// SearchView is the keyword search page.
|
||||
SearchView struct {
|
||||
Layout LayoutView
|
||||
Query string
|
||||
Results []search.Result
|
||||
Total int
|
||||
Indexed int
|
||||
Failed bool
|
||||
}
|
||||
|
||||
// StreamView is the SSE-driven stage demonstration page.
|
||||
StreamView struct {
|
||||
Layout LayoutView
|
||||
Frames int
|
||||
Fragment []LinkView
|
||||
}
|
||||
|
||||
// NotFoundView is the 404 page.
|
||||
NotFoundView struct {
|
||||
Layout LayoutView
|
||||
}
|
||||
)
|
||||
|
||||
// MarkdownBody wraps pre-rendered Markdown HTML.
|
||||
func MarkdownBody(html string) templ.Component { return templ.Raw(html) }
|
||||
|
||||
// SafeSnippet escapes a search snippet but keeps the <mark> tags the index
|
||||
// inserted. Content is in-repo and trusted; escaping is defence in depth.
|
||||
func SafeSnippet(s string) templ.Component {
|
||||
s = stdhtml.EscapeString(s)
|
||||
s = strings.ReplaceAll(s, "<mark>", "<mark>")
|
||||
s = strings.ReplaceAll(s, "</mark>", "</mark>")
|
||||
return templ.Raw(s)
|
||||
}
|
||||
|
||||
// ConfidenceClass maps a confidence value onto a CSS class. The value itself is
|
||||
// always displayed — never softened.
|
||||
func ConfidenceClass(c string) string {
|
||||
switch c {
|
||||
case content.ConfidenceDelivered:
|
||||
return "conf conf-delivered"
|
||||
case content.ConfidenceRunning:
|
||||
return "conf conf-running"
|
||||
case content.ConfidenceInProgress:
|
||||
return "conf conf-in-progress"
|
||||
case content.ConfidenceAbandoned:
|
||||
return "conf conf-abandoned"
|
||||
case content.ConfidenceHistoric:
|
||||
return "conf conf-historic"
|
||||
}
|
||||
return "conf"
|
||||
}
|
||||
|
||||
// ConfidenceNote is the sentence shown under a status badge.
|
||||
func ConfidenceNote(c string) string {
|
||||
switch c {
|
||||
case content.ConfidenceDelivered:
|
||||
return "Delivered."
|
||||
case content.ConfidenceRunning:
|
||||
return "Running today."
|
||||
case content.ConfidenceInProgress:
|
||||
return "In progress — not finished."
|
||||
case content.ConfidenceAbandoned:
|
||||
return "Abandoned. Kept in the record deliberately."
|
||||
case content.ConfidenceHistoric:
|
||||
return "Historic — worked on, not maintained today."
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// LevelLabel names a depth level for the page kicker.
|
||||
func LevelLabel(level int) string {
|
||||
switch level {
|
||||
case 1:
|
||||
return "Level 1 · capability in one screen"
|
||||
case 2:
|
||||
return "Level 2 · what is actually run"
|
||||
case 3:
|
||||
return "Level 3 · artefact"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user