From 1a31db4db94323aeadbf959b3dea51cfda40f36f Mon Sep 17 00:00:00 2001 From: Łukasz Date: Thu, 23 Jul 2026 13:51:18 +0200 Subject: Spec+plan: Addendum B — lectio-web (HTMX UI, selectable themes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third binary lectio-web: hledger-web-style local server, HTMX partial swaps, embedded templates/CSS/htmx (self-contained). Date/lectionary/ version/all controls + passage lookup, driving the shared readings+render core. Five embedded themes (light/dark/sepia/parchment/nord) switchable live, defaulted via config web_theme; web_port config. Builds after the CLI/TUI/README tasks. --- .../plans/2026-07-23-lectio-go-rewrite.md | 90 ++++++++++++++++++++++ .../specs/2026-07-23-lectio-go-rewrite-design.md | 28 +++++++ 2 files changed, 118 insertions(+) (limited to 'docs/superpowers') diff --git a/docs/superpowers/plans/2026-07-23-lectio-go-rewrite.md b/docs/superpowers/plans/2026-07-23-lectio-go-rewrite.md index b4a82d2..a3ab615 100644 --- a/docs/superpowers/plans/2026-07-23-lectio-go-rewrite.md +++ b/docs/superpowers/plans/2026-07-23-lectio-go-rewrite.md @@ -1511,6 +1511,96 @@ renders vernacular only, with a one-line note that Latin is unavailable. `cli`: add global `--lectionary`/`--lang` flags overriding config; everything else is unchanged because both sources yield `[]Section`. +## Addendum B: lectio-web (HTMX web UI with selectable themes) + +A third binary `lectio-web` — an hledger-web-style local server. Build after the +base plan + Addendum A. Consumes `readings.Load` + `render.GatherVersion`; imports +the domain packages, never the reverse. + +### Amendment to Task 10 (config) — web fields +Add to `Config`: `WebTheme string \`toml:"web_theme"\``, `WebPort int \`toml:"web_port"\``. +Defaults: `WebTheme "light"`, `WebPort 0`. Validate `WebTheme` ∈ +`{light,dark,sepia,parchment,nord}`. Add the two lines to the embedded seed +(after `offline`): `web_theme = "light"` and `web_port = 0` with the spec's +comments. (Do this as a small follow-up commit to internal/config.) + +### Task B1: internal/web — HTML render + embedded themes + +**Files:** Create `internal/web/render.go`, `internal/web/render_test.go`, +`internal/web/templates/*.html`, `internal/web/static/themes/{light,dark,sepia,parchment,nord}.css`, +`internal/web/static/htmx.min.js` (download the pinned release). + +**Interfaces:** +- Consumes `liturgy.Section`, `render.GatherVersion`, `render.OfflineVersions`. +- Produces: `web.RenderReadings(secs []liturgy.Section, versions []string, lectionary string) template.HTML` + (the reading pane: per section, a heading + one column per version built from + `render.GatherVersion(v, sec, lectionary)`; verse-number / heading / citation / + refrain wrapped in CSS-class spans so themes restyle them), and + `web.Themes() []string` (the five theme names). Embed templates + CSS + htmx via `go:embed`. + +- [ ] **Step 1: Write the failing test** +```go +func TestRenderReadings(t *testing.T) { + secs := []liturgy.Section{{Heading: "Ewangelia (J 20, 1. 11-18)", PartID: "ewangelia"}} + html := string(RenderReadings(secs, []string{"wuj"}, "new")) + if !strings.Contains(html, "Ewangelia") || !strings.Contains(html, "class=") { + t.Errorf("reading pane missing heading/classes: %q", html[:min(200, len(html))]) + } +} +func TestThemesEmbedded(t *testing.T) { + for _, name := range Themes() { + if b, err := themeCSS(name); err != nil || len(b) == 0 { + t.Errorf("theme %s not embedded", name) + } + } +} +``` +- [ ] **Step 2:** `go test ./internal/web/` → FAIL. +- [ ] **Step 3:** Implement render.go: `//go:embed templates static` FS; parse templates once; `RenderReadings` builds the pane by calling `render.GatherVersion` per (section, version) and feeding a `templates/readings.html` fragment; wrap heading/citation/verse-number/refrain in ``. `Themes()` returns the five names; `themeCSS(name)` reads `static/themes/.css` from the embed FS (error on unknown). Write five real theme CSS files (each defines the colour-role classes + page background/foreground; light/dark/sepia/parchment/nord distinct). Download the pinned `htmx.min.js` into `static/`. +- [ ] **Step 4:** `go test ./internal/web/` → PASS. +- [ ] **Step 5:** Commit `web: HTML render + embedded themes`. + +### Task B2: internal/web — server + handlers + cmd/lectio-web + +**Files:** Create `internal/web/server.go`, `internal/web/server_test.go`, `cmd/lectio-web/main.go`. + +**Interfaces:** +- Consumes `config.Config`, `readings.Load`, `bible.Lookup`, B1's render. +- Produces: `web.NewServer(cfg config.Config) http.Handler`; `web.Run(cfg) error` + (pick port = cfg.WebPort or a free one, start, open the browser). + +- [ ] **Step 1: Write the failing test** (httptest against the handler, no real browser) +```go +func TestIndexAndPartial(t *testing.T) { + liturgy.SetBaseURL(fixtureServerURL(t) + "/liturgia/%s/Ewangelia") // reuse the T8 hook + srv := NewServer(config.Default()) + // GET /?date=2026-07-22&v=wuj -> 200, contains a reading + the theme + htmx script + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, httptest.NewRequest("GET", "/?date=2026-07-22&v=wuj", nil)) + if rec.Code != 200 || !strings.Contains(rec.Body.String(), "htmx") { t.Fatalf("index: %d", rec.Code) } + // GET /readings (HTMX partial) -> 200, fragment only (no ) + rec2 := httptest.NewRecorder() + srv.ServeHTTP(rec2, httptest.NewRequest("GET", "/readings?date=2026-07-22&v=wuj", nil)) + if rec2.Code != 200 || strings.Contains(rec2.Body.String(), " contains the verse + rec3 := httptest.NewRecorder() + srv.ServeHTTP(rec3, httptest.NewRequest("GET", "/lookup?ref=J+20:1&v=wuj", nil)) + if !strings.Contains(rec3.Body.String(), "20:1") { t.Fatalf("lookup missing verse") } + // GET /theme.css?name=sepia -> text/css + rec4 := httptest.NewRecorder() + srv.ServeHTTP(rec4, httptest.NewRequest("GET", "/theme.css?name=sepia", nil)) + if rec4.Code != 200 || !strings.Contains(rec4.Header().Get("Content-Type"), "css") { t.Fatalf("theme.css") } +} +``` +- [ ] **Step 2:** `go test ./internal/web/` → FAIL. +- [ ] **Step 3:** Implement server.go: routes — `/` full page (date/lectionary/version/all/theme controls with `hx-get="/readings"` targeting the pane; the passage-lookup form `hx-get="/lookup"`; theme `` + a theme `