The nav form carried hx-get="/search" hx-target="#search-results", but #search-results exists only on /search. On every other page htmx found no target and silently discarded the response: a search box that looks like it works and does nothing. - SearchForm is now a plain GET with no htmx attributes (navigation and the landing page), so it navigates to /search?q=… from any page and with JavaScript disabled. - SearchFormLive keeps the in-place swap on the search page, where the target does exist. For HX-Request the server now returns the block's *contents* (SearchResultsBody), so the fragment no longer nests a second element with id="search-results" inside the live region. - Empty query shows a prompt instead of an empty list. A zero-match query says "Nothing found" and offers the six areas plus the evidence index. - The result count is role="status" inside the existing aria-live container, so a screen reader reads "4 results for …" rather than a run of titles. Tests (internal/server, internal/site): - the nav form carries no htmx attribute on nine pages, and no built page carries an hx-target whose element is missing (walks all of dist/); - search submitted from /, /edge/ and /platform/ lands on a page with results with no HX-Request header, i.e. the JavaScript-disabled path; - empty query, zero match and the announced count; - a content change followed by a build is visible in dist/search.db. That is the deploy path: deploy/deploy.sh runs `make dist` before it stages a release, and search.Build removes and rewrites the index every build. DoD: search from home, /edge and /platform lands on results; works with JavaScript disabled; zero-match query shows a message; content change followed by a deploy is reflected in search; no htmx request targets a missing element. (cherry picked from commit a56c795768e1e9b10204aae04c3612d7a96775a7)
263 lines
9.0 KiB
Plaintext
263 lines
9.0 KiB
Plaintext
package templates
|
|
|
|
import "strconv"
|
|
|
|
// 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>
|
|
}
|
|
}
|
|
|
|
// DocPage renders an L1, L2 or L3 document.
|
|
templ DocPage(v DocView) {
|
|
@Layout(v.Layout) {
|
|
<article class="doc">
|
|
<header class="doc-head">
|
|
<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 {
|
|
<span class="chip">
|
|
<strong>{ m.Value }</strong>
|
|
<span>{ m.Label }</span>
|
|
</span>
|
|
}
|
|
</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>
|
|
|
|
// Level 1 is one screen: the claim, the figures, the technology and the
|
|
// link in the prose. The evidence table starts at level 2, which is the
|
|
// page that argues for the claim.
|
|
if v.Doc.Level >= 2 && (len(v.Evidence) > 0 || len(v.SubPages) > 0) {
|
|
<section class="doc-evidence" aria-labelledby="evidence-heading">
|
|
<h2 id="evidence-heading">Artefacts</h2>
|
|
if len(v.Evidence) > 0 {
|
|
@EvidenceTable(evidenceRows(v.Evidence))
|
|
}
|
|
if len(v.SubPages) > 0 {
|
|
<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 v.Doc.Level == 2 && 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.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>Last verified { v.Doc.LastVerified } · <code>content/{ v.Doc.Path }</code></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>
|
|
@SearchFormLive(v.Query, "page-search")
|
|
@SearchResults(v)
|
|
</article>
|
|
}
|
|
}
|
|
|
|
// SearchResults is the results block as a page element: the live region wrapper
|
|
// plus its contents, which is what the whole page renders. Without JavaScript the
|
|
// form reloads the page and this markup arrives in the document.
|
|
templ SearchResults(v SearchView) {
|
|
<div id="search-results" aria-live="polite">
|
|
@SearchResultsBody(v)
|
|
</div>
|
|
}
|
|
|
|
// SearchResultsBody is the results block without the live region. This is what an
|
|
// htmx request returns: the search page's form swaps it into #search-results with
|
|
// innerHTML, so returning the wrapper too would nest a second element with the
|
|
// same id inside the first and make the live region announce twice.
|
|
//
|
|
// Everything here arrives inside the polite live region, and the count carries
|
|
// role="status" on top of that, so the number is read as one utterance instead of
|
|
// a run of result titles.
|
|
templ SearchResultsBody(v SearchView) {
|
|
if v.Query == "" {
|
|
<p class="search-empty">Type one or two words above and press Enter. Good starting points: “zigbee”, “signage”, “routing”, “backups”, “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">Nothing found for <q>{ v.Query }</q>. Try a shorter or more general word, or browse an area:</p>
|
|
@searchAreaLinks(v.Areas)
|
|
} else {
|
|
<p class="search-count" role="status">{ 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>
|
|
}
|
|
}
|
|
|
|
// searchAreaLinks is the way out of an empty result: the six areas plus the
|
|
// evidence index, so a failed search never leaves the reader at a dead end.
|
|
templ searchAreaLinks(areas []LinkView) {
|
|
<ul class="search-areas">
|
|
for _, a := range areas {
|
|
<li><a href={ templ.SafeURL(a.Href) }>{ a.Label }</a></li>
|
|
}
|
|
<li><a href="/evidence/">Evidence index</a></li>
|
|
</ul>
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|