Files
the-daily-epub/docs/plans/briefs/ui-pass/handoff-shell.md
T
thalladaandClaude Fable 5.1 5cc4ac4bd3 UI pass: nav state, ears, feeds table, font flash, faster navigation
- Issue pages highlight Archive instead of Latest unless the issue is the
  newest one (IssueView::is_latest), with a router test.
- The ears row is empty by default and says "Dashboard" on dashboard pages;
  "Morning edition" only survives on the empty-state hero.
- Run page: the Feeds card table is fixed-layout and truncates long feed
  names with the full title on hover instead of pushing the entries column
  out of the card.
- Static assets are content-hashed, so serve them immutable for a year;
  preload both Newsreader faces and use font-display: block so a cache
  revalidation never paints the fallback serif first. Persisted details
  state is restored pre-paint from theme.js.
- Cross-document view transitions (140 ms fade, masthead held still) and
  Speculation Rules (prerender reader pages, prefetch dashboard pages on
  hover) served from /static/speculation.json via the Speculation-Rules
  header, which the CSP would otherwise block inline. No client router.
- Dashboard nav gets the same accent underline as the site nav; 40 px
  targets for the theme toggle, account link and dashboard nav.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MD4VWGq6mGcd8Bg67qyx9k
2026-09-04 17:06:48 +00:00

160 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Task 3 handoff — site shell
## What changed
- `src/web/issue.rs`
- `IssueView` now records `is_latest`, computed by `issue::load` from
`Db::latest_issue_date()`.
- The full issue, article, World Briefing, and Behind the paper renderers now
select `latest` or `archive` navigation state from that flag.
- Added a router test with two issue dates proving that only the newest issue
marks Latest `aria-current="page"` and that an older issue marks Archive.
- `src/web/public.rs`
- Anonymous issue rendering uses the same `IssueView::is_latest` navigation
state as signed-in issue rendering.
- `src/web/templates/layout.html`
- The default ears block is empty except on an admin dashboard route, where it
says `Dashboard`. Issue/account/login/archive/error templates retain their
explicit ears; the public empty issue retains its own `Morning edition`.
- Added the italic Newsreader preload alongside the upright preload.
- Named the site header for cross-document view transitions.
- Made the ears-row theme and account/sign-in controls at least 40 px tall;
the account/sign-in link also has a 40 px minimum width.
- Dashboard navigation links now have a 40 × 40 px minimum target and use the same 2 px accent
active underline, ink hover, and inherited focus-visible ring as site nav.
- `src/web/templates/dashboard/run.html`
- The Feeds table no longer uses `.scroll-x`. It is fixed-layout and full
width, truncates feed names with the full value in `title`, and reserves a
narrow fixed entries column. Timings and Provider usage retain their
horizontal-scroll behavior.
- `src/web/tailwind.css`
- Both Newsreader faces use `font-display: block`.
- Added automatic cross-document view transitions with a 140 ms root
cross-fade. The named site header does not fade, and reduced-motion disables
transition animation.
- `.badge`, `.pager`, and `.pagination` use tabular numerals. (`.tile-num`
already did; the brief said `.pager` did too, but the code did not, so it was
corrected.)
- `src/web/static/app.css`
- Rebuilt from `tailwind.css` with Tailwind 4.3.3.
- `src/web/static/speculation.json`
- Added moderate same-origin prerender document rules for `/` and
`/issues/*`, and moderate prefetch rules for `/dashboard` and
`/dashboard/*`.
- Logout, rating, static, and all query-string links are excluded. Excluding
every query is deliberately conservative and therefore also excludes any
query that could mutate state.
- `src/web/mod.rs`
- Serves the speculation rules with
`application/speculationrules+json` and retains content-hash ETags.
- All `/static/*` responses, including 304 responses, now send
`Cache-Control: public, max-age=31536000, immutable`.
- HTML responses send `Speculation-Rules: "/static/speculation.json"` while
retaining their existing public/private/no-store cache behavior.
- Added assertions for immutable cache headers, rules MIME type, both font
preloads, the HTML speculation header, dashboard ears, and dashboard active
underline.
- `src/web/static/app.js`
- A `[data-refresh]` job page now waits for `prerenderingchange` before
scheduling reload, so a prerendered page cannot reload in the background.
- `src/web/static/theme.js`
- Continues to apply the saved theme and `has-js` in the head before first
paint.
- A short-lived mutation observer now restores persisted `details[id]` state
as the parser creates disclosures, then disconnects at `DOMContentLoaded`.
This prevents a cold/delayed `app.js` request from exposing the default
disclosure state before restoration; `app.js` still owns toggle persistence.
## Design and implementation decisions
- Chose `font-display: block` rather than `optional`. This prevents a visible
fallback-to-Newsreader swap and preserves the publication typeface. Preloading
both faces plus immutable content-hashed assets limits the block cost to an
uncached first visit.
- Kept the 140 ms transition restrained and limited to document navigation.
The masthead/nav swap immediately instead of fading, so navigation context
stays visually anchored.
- Added only platform progressive enhancements. There is no fetch/swap router,
HTMX, or route/auth change.
- Speculation Rules and cross-document view transitions are progressive:
Chromium uses them; unsupported browsers ignore them and retain normal
navigation.
## Flash reproduction and visual verification
The requested dev server could not start inside this sandbox:
```text
$ DAILY_EPUB_SERVER__BIND=127.0.0.1:3603 cargo run -- --config ./dev/config.toml serve
Error: could not bind 127.0.0.1:3603: Operation not permitted (os error 1)
```
Therefore the throttled Playwright before/after capture and the requested light,
dark, desktop, and 390 px screenshots could not be produced. The code audit
found that theme and mobile TOC state were already applied by `theme.js` before
paint. Persisted `details` state has now also moved into that pre-paint path.
The other visible-swap candidate was Newsreader's `font-display: swap`; the
preload/cache/block changes address that path.
The requested curl calls likewise could not connect:
```text
$ curl -sS -D - -o /dev/null http://127.0.0.1:3603/
curl: (7) Failed to connect to 127.0.0.1 port 3603 after 0 ms: Couldn't connect to server
$ curl -sS -D - -o /dev/null http://127.0.0.1:3603/static/app.css
curl: (7) Failed to connect to 127.0.0.1 port 3603 after 0 ms: Couldn't connect to server
```
Router tests verify the material headers that those calls should show:
```text
GET /
content-type: text/html; charset=utf-8
cache-control: public, max-age=300
speculation-rules: "/static/speculation.json"
vary: Cookie
GET /static/app.css
content-type: text/css; charset=utf-8
cache-control: public, max-age=31536000, immutable
etag: "0b00513092d50175d3615cfd6cdfc5fed6e416f2aea118c3b0f0110a66089036"
```
Signed-in issue responses remain `private, no-store`; dashboard responses
remain `no-store`.
## Verification
- `cargo fmt -- --check` — passed.
- `cargo clippy --all-targets -- -D warnings` — passed.
- `npm run css:check` — passed with no diff using the repository's pinned
Tailwind 4.3.3 binary. (`npm ci` could not finish under the restricted npm
environment, so the already-installed sibling-worktree binary was placed on
`PATH`; that worktree was only read, never modified.)
- `python3 -m json.tool src/web/static/speculation.json` — passed.
- `node --check src/web/static/app.js` and `node --check
src/web/static/theme.js` — passed.
- `cargo test --lib web::` — 84 passed, 0 failed.
- Dedicated newest/archive navigation test — passed.
- Dedicated dashboard settings shell/ears/active-nav test — passed.
- `cargo test` — 432 library tests passed; 13 tests failed solely because their
mock HTTP servers could not bind loopback (`Operation not permitted`). Ten are
the sandbox failures named in the shared brief; three newer OpenAI mock-server
tests fail at the same listener helper for the same reason.
- `cargo test --lib` with those exact 13 bind-dependent tests skipped — 432
passed, 0 failed, 13 filtered out.
- Non-server integration tests (`config_check`, `e2e_pipeline`, `m2_pipeline`,
`m3_curation`, `m4_epub`) — 25 passed, 0 failed.
- `cargo test --doc` — passed (0 doctests).
- `git diff --check` — passed.
## Left open
- Run the four requested screenshot sets and throttled first-frame comparison
outside the sandbox, where port 3603 can bind. This is also the remaining
browser-level validation for view-transition behavior and Chrome's
Speculation Rules panel.
- `tests/m7_server.rs` was not run because it is explicitly excluded in this
loopback-restricted sandbox.