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
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user