Add briefs for the UI polish pass
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,110 @@
|
||||
# Task 3 — site shell: nav state, ears, runs feeds table, FOUC, faster navigation
|
||||
|
||||
Worktree: `/home/thallada/workspace/the-daily-epub-ui3` (branch `ui-shell`). Dev port 3603.
|
||||
Files you own: `src/web/templates/layout.html`, `src/web/mod.rs` (static assets,
|
||||
headers), `src/web/public.rs`, `src/web/issue.rs` (page/nav plumbing only, not
|
||||
the `Toc`), `src/web/templates/dashboard/run.html`, the "dashboard shell" and
|
||||
`@layer base` parts of `src/web/tailwind.css`, and `src/web/static/theme.js`.
|
||||
Read the shared brief first.
|
||||
|
||||
## 1. "Latest" is highlighted on archived issues (operator issue 5)
|
||||
|
||||
Every issue page (`public::show_issue`, `issue::render_full`, `issue::article`,
|
||||
`world`, `behind`) passes `"latest"` as `active_nav`, so viewing `/issues/2026-09-01`
|
||||
from the archive highlights *Latest* in the site nav. Highlight *Archive*
|
||||
instead whenever the issue is not the newest one. Cleanest: `issue::load` (which
|
||||
has the db) records `is_latest` on `IssueView` via `db.latest_issue_date()`, and
|
||||
each renderer chooses `"latest"`/`"archive"` from it; the public branch in
|
||||
`show_issue` needs the same. Add a router test that fetches an older issue and
|
||||
asserts `aria-current="page"` is on the Archive link and not on Latest (and the
|
||||
reverse for the newest issue).
|
||||
|
||||
## 2. "Morning edition" ears on non-issue pages (operator issue 10)
|
||||
|
||||
`layout.html`'s default `{% block ears %}` prints "Morning edition" — it shows on
|
||||
settings and every other dashboard/account page where it is meaningless. Make the
|
||||
default empty and let issue pages keep overriding it with their dateline. On
|
||||
dashboard pages show a short "Dashboard" label there instead (only when
|
||||
`page.is_admin()` and the active nav is a dashboard section) so the row does not
|
||||
look orphaned; anything else stays empty. Check the row keeps its height and the
|
||||
theme toggle stays right-aligned when the left cell is empty. `issue_public.html`'s
|
||||
empty-state hero may keep its own "Morning edition" eyebrow.
|
||||
|
||||
## 3. Runs page "Feeds (top 20)" overflow (operator issue 6)
|
||||
|
||||
`dashboard/run.html`: the Feeds card wraps its table in `.scroll-x`, whose
|
||||
`> table { min-width: max-content }` lets long feed titles push the *entries*
|
||||
column out of the card until the reader scrolls sideways. That table must fit the
|
||||
card: `table-fixed w-full`, the feed cell `truncate` (nowrap + ellipsis) with the
|
||||
full name in a `title` attribute, entries column a fixed narrow width, no
|
||||
horizontal scroll container (or a `.scroll-x` variant that allows shrinking —
|
||||
your call, but don't break the other tables that rely on `max-content`). Check
|
||||
the Timings and Provider usage cards next to it still look right, and the
|
||||
dashboard overview if it has a similar box.
|
||||
|
||||
## 4. Flash of unstyled content (operator issue 8)
|
||||
|
||||
The operator sees a brief flash of unstyled content when navigating between
|
||||
pages, "or maybe just sometimes". Reproduce before fixing: with the dev server,
|
||||
use Playwright to capture the first frames of a navigation with the cache
|
||||
disabled and a throttled network (there are older experiments `flash.mjs` /
|
||||
`flash2.mjs` in the screenshot directory named in the shared brief you may crib
|
||||
from) and note what actually flashes: the fallback serif before Newsreader loads
|
||||
(`font-display: swap`), a theme flip, the TOC panel, restored `details` state,
|
||||
or something else. Then fix the causes you confirm. Expected fixes:
|
||||
|
||||
- Static assets are already versioned by content hash (`?v={{ page.asset_version }}`),
|
||||
so serve `/static/*` with `Cache-Control: public, max-age=31536000, immutable`
|
||||
(update `static_assets_use_content_hash_etags`); keep the ETag.
|
||||
- Fonts: preload both Newsreader files, and change `font-display` from `swap` to
|
||||
`block` (no visible fallback; with the preload and immutable cache the block
|
||||
period is only ever paid once) — or `optional` if you can show it behaves
|
||||
better here. Explain the choice in the handoff.
|
||||
- Anything initialised by app.js at the end of `<body>` that changes layout
|
||||
(e.g. `details[id]` open state restored from localStorage) should either be
|
||||
decided pre-paint in `theme.js` or made not to shift layout.
|
||||
|
||||
## 5. Faster, SPA-like navigation without a client router (operator issue 9)
|
||||
|
||||
The operator wants link clicks to feel like an instant swap rather than a full
|
||||
reload, but wants to decide on HTMX (boost mode) in a separate effort. **Do not**
|
||||
write a fetch-and-swap router and do not add HTMX. Deliver the standards-based
|
||||
progressive enhancements that get most of the way there:
|
||||
|
||||
- **Cross-document view transitions**: `@view-transition { navigation: auto; }`
|
||||
in `tailwind.css` with a short (~120–150 ms) cross-fade of the page and the
|
||||
header kept stable (`view-transition-name` on the site header so the masthead
|
||||
and nav do not fade; the reader's sticky TOC bar may also stay put). Respect
|
||||
reduced motion (`@media (prefers-reduced-motion: reduce)` → no animation).
|
||||
- **Speculation rules** for prerender/prefetch on hover: inline
|
||||
`<script type="speculationrules">` is blocked by the CSP, so serve a JSON rule
|
||||
set as a static asset (`/static/speculation.json`, embedded like the others)
|
||||
and send the `Speculation-Rules: "/static/speculation.json"` response header
|
||||
on HTML responses (add it in `web::security_headers` or the same layer).
|
||||
Rules: `prerender` with `eagerness: "moderate"` for same-origin reader pages
|
||||
(`/`, `/issues/*`); `prefetch` with `eagerness: "moderate"` for `/dashboard/*`
|
||||
(heavy queries — don't prerender them); exclude `/logout`, `/rate`, `/static/*`
|
||||
and anything with a query string that mutates. Both are Chromium-only today and
|
||||
no-ops elsewhere; say so in the handoff.
|
||||
- Make sure `Cache-Control` on HTML stays as is (`public_cache` / no-store for
|
||||
signed-in) so prerendered pages are not stale for signed-in readers, and that
|
||||
the `[data-refresh]` job-page reload cannot run in a prerendered page
|
||||
(`document.prerendering` guard).
|
||||
|
||||
## Polish while you are there (shell)
|
||||
|
||||
- Site nav and dashboard nav links: consistent focus-visible ring (they inherit
|
||||
the base `a` rule — check it is not clipped by `overflow`), and the dashboard
|
||||
nav's active link should get the same 2 px accent underline treatment as the
|
||||
site nav rather than just a colour change.
|
||||
- Theme toggle and the Sign-in/username link: ≥ 40 px tap targets.
|
||||
- `.tile-num`, `.pager`, and anything that shows changing counts already use
|
||||
tabular numerals — confirm; add to `.badge` when it contains a number.
|
||||
|
||||
## Verification
|
||||
|
||||
`cargo test` (full, minus the sandbox-only failures), `npm run css:check`, and
|
||||
screenshots of `/issues/2026-09-01` (archive highlighting), `/dashboard/settings`
|
||||
(ears), `/dashboard/runs/<id>` (feeds card at desktop and phone width), plus your
|
||||
flash reproduction before/after. Curl the headers of `/` and `/static/app.css`
|
||||
and paste them in `handoff-shell.md`.
|
||||
Reference in New Issue
Block a user