Skip to content

feat(tui): reader paging + verse cursor + book-page layout#11

Merged
ivanmaierg merged 9 commits into
mainfrom
feat/tui-reader-paging
May 12, 2026
Merged

feat(tui): reader paging + verse cursor + book-page layout#11
ivanmaierg merged 9 commits into
mainfrom
feat/tui-reader-paging

Conversation

@ivanmaierg

Copy link
Copy Markdown
Owner

Summary

Builds on PR #10 with the actual reading UX:

  • Pagination: [ / ] move between 15-verse pages within a chapter (silent clamp at edges).
  • Verse cursor: / move a marker between verses; rolls across page boundaries; cursor verse rendered in accent blue (matches docs/ui-sketches.md line 148 — "Focused verse: accent color on the line + ▶ marker in the gutter").
  • Chapter nav rebound: n / p take over chapter advance/retreat from [ / ]. Chapter transitions reset cursor to verse 1.
  • Palette key-conflict fixed: driver gates reader keybinds behind state.kind !== "awaiting", so / (and others) no longer fire while typing into the input. q / Q quit always works regardless of state.
  • getChapter use case: new application use case returns the whole chapter regardless of ref.verses. Single-verse refs like john 3:16 now drop you into the chapter page with the cursor positioned on verse 16 instead of showing only that verse.
  • Book-page layout: bordered frame capped at 70 chars centered horizontally; 5-char prefix matching the original sketch; blank row between verses for vertical rhythm; box auto-sizes to content height instead of stretching to the terminal floor.
  • Welcome hint updated to show the new keymap.

What does NOT change

  • Domain layer untouched apart from parseReference (already shipped in PR feat(tui): interactive reader — palette + fetch + bordered frame #10).
  • Application layer: getPassage stays for the CLI. getChapter is new and reader-only.
  • API adapter unchanged.
  • ADR 0010 TS-native dialect retained (plain useReducer, object-dispatch handler table per Rule 13, no useless comments per Rule 14).

Verify

Original SDD verify: PASS WITH WARNINGS (0 critical / 1 warning / 1 suggestion). Four post-verify user-directed UX iterations were applied on the same branch after manual PTY smoke:

  1. fbd7d71 — Whole-chapter fetch + cursor positioning (john 3:16 → page 2 cursor on verse 16, not single-verse view); palette input width capped; gutter accent washout fix.
  2. 5a5ef32 — Page size 8 → 15; word-wrap with hanging indent; tests refactored to use VERSES_PER_PAGE symbol.
  3. 0ac25aa — Book-page layout: capped 70-char column, horizontal centering, blank-line verse rhythm, content-sized box.
  4. `(HEAD)` — Focused verse uses accent color instead of INVERSE (no more black-on-light block on dark terminals).

bun test: 156/156 pass. bun run tsc --noEmit: exits 0.

Test plan

  • bun test 156/156
  • Manual PTY smoke confirmed by user (palette, fetch, paging, cursor nav, chapter nav, palette reopen mid-reading, quit from each state)

Out of scope (next PR candidates)

  • Dynamic VERSES_PER_PAGE from terminal height
  • Cross-chapter cursor flow (advance past last verse triggers ChapterAdvanced)
  • Two-page open-book spread for wide terminals
  • Visual feedback on page-boundary clamp
  • Long-word wrap (single word > wrap width currently overflows)

Full SDD trail in `openspec/changes/archive/tui-reader-paging/`.

Captures explore/proposal/spec/tasks/verify-report for the paging +
verse cursor change. Verify verdict: PASS WITH WARNINGS (0 critical /
1 warning / 1 suggestion). 151/151 tests green, tsc clean.

Pending: manual PTY smoke for visual cursor + keybind round-trips.
Suggestion to verify at smoke: gutter ▶ uses fg={ACCENT_HEX} AND
attributes={DIM} together — DIM may suppress accent color on some
terminals.
Two UX issues from manual smoke:

1. Single-verse refs like 'john 3:16' showed only that verse instead
   of dropping into the chapter page with the cursor on verse 16.
   Root cause: useEffect called getPassage, which sliced the chapter
   down to ref.verses range.

   Fix: add a new application use case getChapter that returns the
   full chapter regardless of ref.verses, and have the reader call
   it instead. The verse range now serves as the initial cursor
   target, not a slice predicate. PassageFetched computes
   cursorIndex by looking up the target verse number in the fetched
   passage and derives pageStartIndex from cursorIndex / VERSES_PER_PAGE.

   Chapter nav (n/p) resets ref.verses to {1, 1} so the cursor
   lands on verse 1 of the new chapter rather than carrying the
   old verse number into a possibly-shorter chapter.

2. The palette input spanned the whole bordered frame. Wrapped it
   in a width=50 box with marginLeft=2 so it reads as an input
   field, not the entire reader surface.

Also fixes the verify suggestion: the focused ▶ cursor span was
applying both fg={ACCENT_HEX} and attributes={DIM}, which washed
out the accent on most terminals. DIM now applies only when the
gutter is empty (non-focused rows).

Tests: 151 → 156 (+5: 3 for getChapter use case, 2 for cursor
positioning in PassageFetched). tsc clean.
Two UX bumps:

1. VERSES_PER_PAGE 8 → 15. Closer to the original ui-sketches.md
   Reading view density. Comfortably fits an 80×24 terminal once
   word-wrap accounts for the chrome.

2. Word-wrap with hanging indent for verse text. Previously, long
   verses ran off the right edge or wrapped break-anywhere with no
   visual continuity. Now: each verse wraps at word boundaries, the
   first line carries the gutter + verse number, and continuation
   lines indent 6 chars to align with the text column — matching the
   original docs/ui-sketches.md sketch shape (line 122).

Wrap width derives from useTerminalDimensions() each render, so
narrow terminals reflow correctly. Floor at 20 chars to keep the
wrap useful on tiny terminals.

Tests updated to be VERSES_PER_PAGE-relative (using the constant
symbol in place of hardcoded 8/16) so future tuning won't churn
the suite. 156/156 still pass, tsc clean.
…e rhythm

Matches docs/ui-sketches.md Reading view (line 122):

- Constrain bordered frame to max 70 chars (configurable via
  PAGE_MAX_WIDTH), centered horizontally. Wide terminals no longer
  produce a single 200-char box that runs verses off the right edge
  or floods the screen with empty margin.
- Falls back to terminal width - 4 on narrower terminals (floor 40).
- 5-char prefix (' 16 ' or '▶16 ') with 5-space hanging indent —
  matches the sketch's verse-row layout exactly.
- Blank row between verses for vertical breathing room (also from
  the sketch).
- Top padding + box auto-sizes to content so the box no longer
  stretches to the terminal floor and looks like a log dump.

Tests: 156/156 still pass (no reducer changes). tsc clean.
INVERSE swaps fg/bg, which on a dark terminal renders as black-on-
light — jarring against the rest of the muted page. Switching to
fg=ACCENT_HEX, matching docs/ui-sketches.md line 148 'Focused verse:
accent color on the line + ▶ marker in the gutter'.

The Typography table at line 437 of the same doc said 'selection
(inverse)', but the Reading view rules are more specific to this
surface and read better on a dark background.

Verse text and gutter marker now share the accent token when focused,
making the cursor row read as a single highlighted unit instead of
a sudden color block.
SDD cycle closed. PASS WITH WARNINGS verify + 4 user-sanctioned
post-verify iterations from manual smoke (whole-chapter fetch,
denser pages, book-page layout, accent cursor color).

156/156 tests pass. Manual PTY smoke confirmed by user.

Out-of-scope follow-ups: dynamic page size from terminal height,
cross-chapter cursor flow, two-page open-book spread, long-word
wrap, visual page-boundary feedback.
@ivanmaierg ivanmaierg merged commit 244163d into main May 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant