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)
201 lines
4.8 KiB
Go
201 lines
4.8 KiB
Go
// 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.
|
|
//
|
|
// The generated *_templ.go files are not committed; `make generate` writes them
|
|
// from the repository root before every build. There is deliberately no
|
|
// //go:generate directive: templ records the .templ path in its error messages,
|
|
// so generating from a package directory and generating from the root produce
|
|
// different files. One entry point keeps the output byte-identical.
|
|
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
|
|
Role string
|
|
Body templ.Component
|
|
Metrics []MetricView
|
|
Tech []string
|
|
Evidence []LinkView
|
|
Backlinks []LinkView
|
|
SubPages []LinkView
|
|
Timeline []TimelineItem
|
|
}
|
|
|
|
// 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
|
|
// Areas are the six area landings, offered as the way out of an
|
|
// empty result. Nothing here was ever a search hit.
|
|
Areas []LinkView
|
|
}
|
|
|
|
// 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"
|
|
}
|