diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/plans/2026-07-23-lectio-go-rewrite.md | 90 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-07-23-lectio-go-rewrite-design.md | 28 |
2 files changed, 118 insertions, 0 deletions
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 `<span class="...">`. `Themes()` returns the five names; `themeCSS(name)` reads `static/themes/<name>.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 <link> + 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 <html>) + 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(), "<html") { t.Fatalf("partial not a fragment") } + // GET /lookup?ref=J+20:1&v=wuj -> 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 `<link id=theme href="/theme.css?name=…">` + a theme `<select>` that swaps it; embedded `/static/htmx.min.js`), `/readings` HTMX partial (calls `readings.Load` with query params, offline→OfflineVersions, returns `RenderReadings`), `/lookup` (bible.Lookup for the typed ref across the picked versions → HTML fragment), `/theme.css?name=` (serves `themeCSS`, `Content-Type: text/css`), `/static/` (embedded). `Run(cfg)` listens on cfg.WebPort or `:0`, prints the URL, best-effort opens the browser (`xdg-open`/`open`/`start`), serves. `cmd/lectio-web/main.go`: `config.Load()` → `web.Run(cfg)`. +- [ ] **Step 4:** `go test ./internal/web/` → PASS; `go build ./cmd/lectio-web`; manual: run it, open the browser, click date/version/theme, try the lookup box. +- [ ] **Step 5:** Commit `web: HTMX server + lectio-web binary`. + +### Amendment to Task 1/14 (Makefile, .gitignore, README) +Add `lectio-web` to the Makefile `build`/`install`/`cross` targets and `.gitignore`; +document `lectio-web`, its keys/controls, and the themes in the README. + ## Self-Review Notes - Spec coverage: two binaries (T1,12,13,15-via-14), embedded corpora (T1,4), lookup (T4,5), aliases incl. `J`→John (T3), citation conversion + psalm systems (T2,6), fetch/parse (T7,8), cache HTML+JSON (T8), sigla harvest + offline (T9), config incl. `offline` (T10), render + dedup + pl→wuj (T11), subcommands (T12), colored reader TUI (T13), Makefile/cross/README (T1,14). All spec sections map to a task. diff --git a/docs/superpowers/specs/2026-07-23-lectio-go-rewrite-design.md b/docs/superpowers/specs/2026-07-23-lectio-go-rewrite-design.md index 2c6824b..b8f8c81 100644 --- a/docs/superpowers/specs/2026-07-23-lectio-go-rewrite-design.md +++ b/docs/superpowers/specs/2026-07-23-lectio-go-rewrite-design.md @@ -287,6 +287,32 @@ light and dark terminals; a `NO_COLOR` env / non-TTY output degrades to plain. Exact hues are finalised in planning, but the role → style mapping above is fixed. The CLI stays plain text (colour is a TUI concern). +## Web UI (`lectio-web`, `internal/web`) + +A third binary — an `hledger-web`-style local companion. Run `lectio-web`; it +starts a local HTTP server and opens the browser at `http://localhost:<port>` +(`web_port`, 0 = auto-pick). Pure Go, everything embedded (`go:embed`), still a +self-contained single binary — no cgo, no build step. + +- **Interactivity: HTMX.** Server-rendered `html/template` pages; controls carry + `hx-get`/`hx-post` and swap only the reading pane via handlers that return HTML + partials. HTMX (~14 KB) is embedded. State lives in URL query params + (bookmarkable, back-button works). No SPA, no npm. +- **UI.** A top bar with: date `←/→` + date picker; lectionary toggle + (new/traditional); version checkboxes (pl/wuj/vul/grb/drb) driving the compare + columns; all-parts vs gospel toggle; a **theme** selector; and a passage + lookup box (`bible.Lookup` over the embedded corpora — type `J 20:1`, pick + versions, see it). The main pane shows the day's readings as one version or + responsive compare columns. +- **Themes.** A small set of embedded CSS themes, switchable live in the UI and + defaulted by `web_theme`: `light`, `dark`, `sepia`, `parchment`, `nord`. The + colour roles (heading / citation / verse number / refrain) are CSS classes each + theme restyles. The active theme is stored in a cookie; `web_theme` is the + initial default. +- **Reuse.** Consumes the same core: `readings.Load` → `[]liturgy.Section`, + `render.GatherVersion`'s `(label, blocks)` fed into templates. No changes to + the domain packages — `internal/web` imports them, never the reverse. + ## Config (`internal/config`) `Config` struct loaded via go-toml. Resolution: `LECTIO_CONFIG` env → @@ -302,6 +328,8 @@ default_version = "pl" # TUI start / `lectio show` default width = 0 # CLI wrap width; 0 = detect terminal all = false # default to all parts (true) or just the gospel (false) offline = false # true = never fetch; read only harvested sigla + cache +web_theme = "light" # lectio-web default theme: light|dark|sepia|parchment|nord +web_port = 0 # lectio-web port; 0 = auto-pick a free port # Which parts to show. Both tables are commented out -> every part is shown. # Uncomment a table and set a part to false to hide it; parts you don't list |
