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,93 @@
|
|||||||
|
# Shared brief — UI polish pass (2026-09-04)
|
||||||
|
|
||||||
|
The operator asked for a design/bug-fix pass over the web dashboard v2 (Tailwind
|
||||||
|
redesign, merged to `main` at `e719e03`+). The work is split into three tasks
|
||||||
|
(`01-toc.md`, `02-rating.md`, `03-shell.md`) that run in parallel in separate git
|
||||||
|
worktrees. Read `docs/plans/briefs/web-dashboard-v2/00-shared.md` for the design
|
||||||
|
language (quiet morning paper: warm paper, near-black ink, one red accent,
|
||||||
|
hairline rules, real typographic hierarchy; reader pages serif, dashboard sans)
|
||||||
|
and the Tailwind v4 conventions. This brief overrides it where they disagree.
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
- Work only in the worktree named in your brief. **Never touch the main checkout**
|
||||||
|
at `/home/thallada/workspace/the-daily-epub`. Do not `git commit` (the sandbox
|
||||||
|
cannot write `.git/worktrees`); leave the work in the tree and the orchestrator
|
||||||
|
commits. If you can commit, one commit on the worktree branch is fine.
|
||||||
|
- Styling lives in **`src/web/tailwind.css`** (Tailwind v4, `@apply` in
|
||||||
|
`@layer components`, semantic colour tokens `paper/paper-2/ink/ink-2/muted/rule/
|
||||||
|
rule-strong/accent/loved/good/down/warn`) and in Tailwind utility classes in the
|
||||||
|
askama templates under `src/web/templates/`. **Rebuild the committed CSS** after
|
||||||
|
every change with `npm run css` (writes `src/web/static/app.css`; the binary
|
||||||
|
embeds it with `include_str!`, so rebuild Rust after the CSS). `npm run css:check`
|
||||||
|
must print no diff at the end.
|
||||||
|
- CSP is `default-src 'self'; style-src 'self'; script-src 'self'`: **no inline
|
||||||
|
`<style>`/`<script>`, no `style=""` attributes, no CDNs.** Setting CSS custom
|
||||||
|
properties or `element.style.*` from `app.js` (CSSOM) is allowed by CSP but
|
||||||
|
prefer class/attribute toggles styled from `tailwind.css`.
|
||||||
|
- JS is plain `src/web/static/app.js` (runs once at the end of `<body>`) and the
|
||||||
|
tiny pre-paint `src/web/static/theme.js`. Keep every hook app.js relies on:
|
||||||
|
`form.rating`, `button[data-label]`, `.active` + `aria-pressed` on rating
|
||||||
|
buttons, `button.clear`, `form[data-confirm]`, `details[id]`, `table[data-filter]`,
|
||||||
|
`.table-filter`, `button[data-reset]`/`data-default`, `[data-refresh]`,
|
||||||
|
`[data-toc-panel]`, `[data-toc-toggle]`, `[data-toc-progress]`, `[data-toc-scroll]`,
|
||||||
|
`[data-theme-toggle]`. Keep `id`/`name`/`action`/`aria-*` attributes used by
|
||||||
|
handlers or tests. Router tests assert on text and some class names; if a test
|
||||||
|
breaks only because a cosmetic class changed, update the test.
|
||||||
|
- Don't change routes, auth, migrations or `web::security_headers`.
|
||||||
|
- Prefer CSS transitions (interruptible) over keyframes; name exact transition
|
||||||
|
properties (never `transition: all`); respect the existing
|
||||||
|
`prefers-reduced-motion` rule. Touch targets ≥ 44 px on phones (extend with a
|
||||||
|
pseudo-element if the visible control is smaller); ≥ 40 px in the dense
|
||||||
|
dashboard. Tabular numerals for changing numbers. Hairline rules, not boxes,
|
||||||
|
for structure. No decorative motion on frequent interactions.
|
||||||
|
- Keep `cargo fmt`, `cargo clippy --all-targets -- -D warnings` and the tests
|
||||||
|
green. Iterate with narrow runs (`cargo test --lib web::`), one `cargo test` at
|
||||||
|
the end. **Sandbox note:** inside the Codex sandbox `bind()` on 127.0.0.1 is
|
||||||
|
forbidden, so these pre-existing tests fail there and must be ignored:
|
||||||
|
`curate::llm::tests::anthropic_*` (4), `extract::tests::relative_urls_resolve_against_the_url_we_landed_on`,
|
||||||
|
`server::tests::*` (5) and `tests/m7_server.rs`. Do not "fix" them.
|
||||||
|
- This host has 4 cores / 7 GB and other agents build Rust at the same time; be
|
||||||
|
patient with `cargo` and don't run several builds concurrently yourself.
|
||||||
|
- Don't gold-plate and don't widen scope beyond your brief's numbered items plus
|
||||||
|
the polish items it lists. Where the brief and the code disagree, follow the
|
||||||
|
code and note the deviation.
|
||||||
|
- Finish by writing `docs/plans/briefs/ui-pass/handoff-<task>.md`: what changed
|
||||||
|
(file by file, briefly), design decisions, deviations, what you verified and
|
||||||
|
how (commands, screenshots), anything left open.
|
||||||
|
|
||||||
|
## Previewing
|
||||||
|
|
||||||
|
A seeded dev database exists in the main checkout at
|
||||||
|
`/home/thallada/workspace/the-daily-epub/dev` (two issues: No. 18 on 2026-09-01,
|
||||||
|
legacy; No. 19 on 2026-09-02, full snapshot; accounts `admin`/`adminpassword123`
|
||||||
|
and `reader`/`readerpassword123`). Copy it if you need your own:
|
||||||
|
`cp -r /home/thallada/workspace/the-daily-epub/dev ./dev` (git-ignored). Run your
|
||||||
|
worktree's server on **your own port** (given in your brief; 3499 is production,
|
||||||
|
3599 is the orchestrator's):
|
||||||
|
|
||||||
|
```text
|
||||||
|
DAILY_EPUB_SERVER__HMAC_SECRET=$(head -c 48 /dev/urandom | base64) \
|
||||||
|
DAILY_EPUB_SERVER__BIND=127.0.0.1:<port> \
|
||||||
|
cargo run -- --config ./dev/config.toml serve
|
||||||
|
```
|
||||||
|
|
||||||
|
(the listen address is `server.bind` in `./dev/config.toml`, overridable by the
|
||||||
|
env var above; your worktree's `dev/config.toml` is already set to your port. If
|
||||||
|
the sandbox forbids binding, rely on router tests + `cargo test` and say so in
|
||||||
|
the handoff).
|
||||||
|
|
||||||
|
Screenshots: `node shoot.mjs <outdir> [--user admin|reader|none] [--theme light|dark] [--mobile] [--full] /path …`
|
||||||
|
with `BASE=http://127.0.0.1:<port>`, from
|
||||||
|
`/tmp/claude-1000/-home-thallada-workspace-the-daily-epub/553c7d77-d8f4-44f8-a379-73cedea23814/scratchpad/shoot/`
|
||||||
|
(Playwright with a headless Chromium already installed under `~/.cache/ms-playwright`).
|
||||||
|
If that directory is unreadable, the bare browser at
|
||||||
|
`~/.cache/ms-playwright/chromium_headless_shell-1208/chrome-headless-shell-linux64/chrome-headless-shell --headless --no-sandbox --screenshot=out.png --window-size=1280,1400 URL`
|
||||||
|
works for anonymous pages. **Look at your screenshots** (read the PNGs) before
|
||||||
|
calling a design change done, in light and dark, desktop and 390 px phone.
|
||||||
|
|
||||||
|
Useful dev URLs: `/` (latest issue, No. 19), `/issues/2026-09-01` (archived
|
||||||
|
issue), `/issues/2026-09-02/articles/<id>` (chapter; pick ids from the TOC),
|
||||||
|
`/issues/2026-09-02/world`, `/issues/2026-09-02/behind`, `/dashboard`,
|
||||||
|
`/dashboard/runs`, `/dashboard/runs/<id>`, `/dashboard/ratings`,
|
||||||
|
`/dashboard/settings`, `/dashboard/articles`.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Task 1 — table-of-contents sidebar: mobile height, scroll sync, scrollbar
|
||||||
|
|
||||||
|
Worktree: `/home/thallada/workspace/the-daily-epub-ui1` (branch `ui-toc`). Dev port 3601.
|
||||||
|
Files you own: `src/web/templates/_toc.html`, the TOC parts of
|
||||||
|
`src/web/static/app.js` (the `[data-toc-*]` blocks), the "table-of-contents"
|
||||||
|
section of `src/web/tailwind.css`, and the index entries in
|
||||||
|
`src/web/templates/issue_full.html` (only to add data attributes / ids). Backend:
|
||||||
|
`src/web/issue.rs` `Toc`/`TocItem` if a field helps. Read the shared brief first.
|
||||||
|
|
||||||
|
The signed-in issue pages (`issue_full.html`, `article.html`, `world.html`,
|
||||||
|
`behind.html`) share `_toc.html`: a sticky bar with a hamburger below `lg`, a
|
||||||
|
sticky sidebar at `lg+`. Chapters are separate pages; the issue front page lists
|
||||||
|
every chapter in "In This Issue".
|
||||||
|
|
||||||
|
## 1. The open hamburger panel must fill the screen (operator issue 1)
|
||||||
|
|
||||||
|
Today the panel is `absolute top-full max-h-[72vh]`, so on a phone it stops
|
||||||
|
partway down with page content visible beneath it — it looks broken. Make the
|
||||||
|
open panel extend from the bottom of the sticky bar to the **bottom of the
|
||||||
|
viewport** (use `100dvh`, not `100vh`, so mobile browser chrome is accounted
|
||||||
|
for), with the chapter list scrolling inside it and the bar staying fixed above
|
||||||
|
it. Handle both states: the bar already stuck at the top of the viewport, and
|
||||||
|
the bar still in flow below the masthead on first open. Prefer a pure-CSS
|
||||||
|
solution; if the bar's on-screen position must be measured, set one CSS custom
|
||||||
|
property from app.js on open/resize and consume it in `tailwind.css`. Avoid
|
||||||
|
jumps of the underlying page when the panel opens or closes. Keep Escape, the
|
||||||
|
outside-click close, the tap-on-chapter close, and the `lg` breakpoint reset.
|
||||||
|
Content behind the panel must not scroll while it is open (e.g. toggle a class
|
||||||
|
on `<html>` that sets `overflow:hidden`, cleared on close).
|
||||||
|
|
||||||
|
## 2. Scroll-synced current chapter (operator issue 2)
|
||||||
|
|
||||||
|
On the issue front page the reader scrolls through "In This Issue"; each index
|
||||||
|
entry corresponds to a TOC chapter. As the reader scrolls, the TOC's current
|
||||||
|
marker must follow: when entry 5's card reaches the top region of the viewport,
|
||||||
|
chapters 1–4 have visibly "passed" and 5 is current. Implement a scroll-spy in
|
||||||
|
app.js (IntersectionObserver or a rAF-throttled scroll listener; either is fine,
|
||||||
|
but it must be cheap and passive):
|
||||||
|
|
||||||
|
- Mark each index entry `<li>` in `issue_full.html` with the chapter it
|
||||||
|
represents (e.g. `data-toc-entry="{{ entry.href }}"`, matching the TOC link's
|
||||||
|
`href`); "The Brief" section maps to the Brief item, the colophon to the
|
||||||
|
Colophon item.
|
||||||
|
- The current entry is the last one whose top edge is above a threshold
|
||||||
|
(roughly the sticky bar height + ~1/3 of the viewport). Before the first entry
|
||||||
|
the Brief is current.
|
||||||
|
- Move the current styling to a single hook so JS only toggles one attribute:
|
||||||
|
give TOC links a class (say `toc-link`) and style `.toc-link[aria-current]`
|
||||||
|
in `tailwind.css` (keep `aria-current="page"` for the server-rendered chapter
|
||||||
|
on chapter pages; the scroll-spy may use `aria-current="location"`). Passed
|
||||||
|
chapters may stay ink-coloured while unread ones are `ink-2`; keep it subtle.
|
||||||
|
- Update the sticky mobile bar's label and "N / total" counter and both
|
||||||
|
`[data-toc-progress]` bars from the same source of truth (the front page
|
||||||
|
currently leaves the progress bar at 0; it should reflect the current chapter
|
||||||
|
as the reader passes entries). The desktop "Chapter N of M / Front page" line
|
||||||
|
should update too. Chapter pages keep today's progress-within-chapter logic.
|
||||||
|
- When the current item changes, **auto-scroll the TOC panel** so the current
|
||||||
|
link is comfortably in view (there is already a `revealCurrent()`; reuse it,
|
||||||
|
make it use `scrollTo({behavior:"smooth"})` unless `prefers-reduced-motion`,
|
||||||
|
and make sure it works for the desktop sticky sidebar as well as the open
|
||||||
|
mobile panel). Never scroll the window itself from this code.
|
||||||
|
- Chapter pages already mark the chapter current; make sure the sidebar reveals
|
||||||
|
it on load (desktop and mobile) — verify on a long TOC.
|
||||||
|
|
||||||
|
## 3. Scrollbar must not overlap the TOC text (operator issue 11)
|
||||||
|
|
||||||
|
In the sidebar the scrollbar is painted over the right edge of the chapter
|
||||||
|
labels. Reserve the gutter (`scrollbar-gutter: stable`, plus right padding on
|
||||||
|
the panel so the list ends before the gutter) so the scrollbar sits to the
|
||||||
|
right of the content on every platform, including overlay-scrollbar macOS.
|
||||||
|
Keep `scrollbar-width: thin` and the rule-coloured thumb.
|
||||||
|
|
||||||
|
## Polish while you are there
|
||||||
|
|
||||||
|
- Chapter links: ≥ 44 px tall tap targets on phones (they are `py-2.5` today,
|
||||||
|
check), hover/focus-visible states consistent with the nav.
|
||||||
|
- The hamburger icon should animate to a close icon while open using the
|
||||||
|
cross-fade recipe (both SVGs in the DOM, one absolutely positioned; opacity
|
||||||
|
`0→1`, scale `0.25→1`, blur `4px→0`, ~150 ms, `cubic-bezier(0.2,0,0,1)`), and
|
||||||
|
the `aria-label` should flip to "Close contents".
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Screenshot the front page and one chapter, desktop + phone, light + dark, with
|
||||||
|
the panel open on the phone; script a scroll on the front page (Playwright
|
||||||
|
`page.mouse.wheel` or `window.scrollTo`) and screenshot the sidebar at two
|
||||||
|
scroll positions to prove the marker moves and the panel auto-scrolls. Run the
|
||||||
|
web router tests. Write `handoff-toc.md`.
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# Task 2 — rating widget, issue section headers, ratings-table verdict column
|
||||||
|
|
||||||
|
Worktree: `/home/thallada/workspace/the-daily-epub-ui2` (branch `ui-rating`). Dev port 3602.
|
||||||
|
Files you own: `src/web/templates/_rating_widget.html`, the `.rating*`,
|
||||||
|
`td .rating`, `.prose-body` and reader-typography parts of `src/web/tailwind.css`,
|
||||||
|
`src/web/templates/issue_full.html` and `issue_public.html` (section headers),
|
||||||
|
`src/web/templates/article.html` (rating placement only),
|
||||||
|
`src/web/templates/dashboard/ratings.html`, and the rating `submit` handler in
|
||||||
|
`src/web/static/app.js` if the markup change needs it. Read the shared brief first.
|
||||||
|
|
||||||
|
## 1. Redesign the rating widget (operator issue 3)
|
||||||
|
|
||||||
|
`_rating_widget.html` renders "Was this a good pick? [Loved it][Good][Not for me]
|
||||||
|
clear" (+ a Note input on dashboard pages). The buttons are 40 px tall,
|
||||||
|
full-border segmented, solid-ink when active — too heavy next to the serif index
|
||||||
|
cards on the issue front page and the article footer; they read as a form, not as
|
||||||
|
part of the paper. Redesign it to be cohesive with the article display:
|
||||||
|
|
||||||
|
- Quieter and smaller: sans, `text-xs`/`text-[0.8rem]`, compact height (~30–32 px
|
||||||
|
visible; extend the hit area to 44 px on phones with a pseudo-element), hairline
|
||||||
|
`rule` borders or a single underlined-text style, `rounded-sm`.
|
||||||
|
- The active state should use the semantic colour of the verdict (`loved`, `good`,
|
||||||
|
`down`) as a tint, like `.badge.loved` etc. (`color-mix(in oklab, var(--loved)
|
||||||
|
14%, transparent)` background + coloured text/border), not solid ink — so the
|
||||||
|
chosen verdict is legible at a glance and matches the badges used elsewhere.
|
||||||
|
- The prompt "Was this a good pick?" becomes a small serif-italic or muted-sans
|
||||||
|
lead-in that sits on the same line as the buttons at desktop widths and above
|
||||||
|
them on phones; "clear" stays a quiet text link, visible only when a verdict is
|
||||||
|
active (still in the DOM for the handler; use `[hidden]` or an opacity class).
|
||||||
|
- Keep the markup contract: `form.rating`, `button[type=submit][name=label][value][data-label]`,
|
||||||
|
`.active`, `aria-pressed`, `button.clear`, `label.rating-note > input[name=note]`,
|
||||||
|
the three hidden inputs, `widget.show_note`, and the JSON fetch in app.js (it
|
||||||
|
toggles `.active`/`aria-pressed`; if you add a per-verdict tint class, derive it
|
||||||
|
from `data-label` in CSS — `.rating button.active[data-label="loved"]` — so JS
|
||||||
|
needs no change).
|
||||||
|
- Press feedback: `active:scale-[0.96]` with `transition-property: transform,
|
||||||
|
background-color, color, border-color` on the buttons.
|
||||||
|
|
||||||
|
Check both placements: the index card on `/` (after "Why it's here") and the
|
||||||
|
article footer on a chapter page; also `/dashboard/articles/<id>` which shows the
|
||||||
|
widget with the note field.
|
||||||
|
|
||||||
|
## 2. Section headers in the issue (operator issue 4)
|
||||||
|
|
||||||
|
On the issue front page ("In This Issue") and the public issue page, each section
|
||||||
|
name (e.g. *Technology*) is a 0.72 rem uppercase muted label above a hairline —
|
||||||
|
the same style as tiny eyebrows elsewhere — so readers scroll past it without
|
||||||
|
noticing the section changed. Make section headers read as headers: serif,
|
||||||
|
`text-2xl` or so, weight 600, `leading-[1.1]`, `tracking-[-0.01em]`,
|
||||||
|
`text-wrap:balance`, sitting on a **stronger rule** (`border-t-2 border-rule-strong`
|
||||||
|
or a double rule like the masthead) with generous space above, and a small
|
||||||
|
uppercase eyebrow ("Section" or the article count, optional) if it helps
|
||||||
|
hierarchy. Headlines inside the section stay `text-2xl`/lead `text-3xl`, so the
|
||||||
|
section header must be distinguishable from them by the rule + spacing + smaller
|
||||||
|
size rather than by being bigger; consider `text-xl` uppercase-tracked serif
|
||||||
|
small caps if that reads better — screenshot both and pick one. Apply the same
|
||||||
|
treatment to "The Brief" and "Colophon" headers on the front page so the page has
|
||||||
|
one heading system, and to the section names on `issue_public.html`. Leave the
|
||||||
|
TOC sidebar's section labels alone (task 1 owns that file).
|
||||||
|
|
||||||
|
Also check article bodies (`.prose-body h2/h3` on a chapter page): they should be
|
||||||
|
clearly headings against `1.0625rem` body text; adjust only if they are weak.
|
||||||
|
|
||||||
|
## 3. Ratings table verdict column (operator issue 7)
|
||||||
|
|
||||||
|
`dashboard/ratings.html`, tab "Current": the first column stacks a verdict badge,
|
||||||
|
the three rating buttons, a Note input and "clear" — busy and janky. Make that
|
||||||
|
cell one calm unit:
|
||||||
|
|
||||||
|
- Drop the separate badge: the tinted active button (from item 1) *is* the verdict.
|
||||||
|
If a row is `cleared` show a muted "cleared" state in the group instead.
|
||||||
|
- A compact segmented group of the three verdicts on one line (fixed width, e.g.
|
||||||
|
`w-[13rem]`), the "clear" link tucked at the end of that line, and the note
|
||||||
|
field beneath as a quiet single-line input (`placeholder="Add a note…"`,
|
||||||
|
bottom-hairline only, no box) that only shows a border on focus. Everything
|
||||||
|
left-aligned on one grid so rows line up; no wrapping inside the cell at the
|
||||||
|
table's minimum width.
|
||||||
|
- Vertical rhythm: the cell content top-aligned with the row's article title.
|
||||||
|
- Keep the note column of the table as is (it shows the stored note).
|
||||||
|
|
||||||
|
## Polish while you are there (reader pages)
|
||||||
|
|
||||||
|
- `text-wrap: balance` on headlines (`h1`–`h4` in reader pages, TOC excluded);
|
||||||
|
`text-wrap: pretty` on `.prose-body p` and index summaries.
|
||||||
|
- `.prose-body img`: add the depth outline `outline: 1px solid oklch(0 0 0 / 0.1)`
|
||||||
|
in light and `oklch(1 0 0 / 0.1)` in dark (pure black/white only, use the
|
||||||
|
existing `dark` variant), `outline-offset: -1px`.
|
||||||
|
- Buttons `.btn`, `.btn-primary`, `.btn-danger`: `active:scale-[0.96]` with named
|
||||||
|
transition properties (transform + colours), keeping the reduced-motion rule.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Screenshot `/` (signed in as reader), a chapter page footer, `/dashboard/ratings`
|
||||||
|
(admin, tab Current) and `/dashboard/articles/<id>`, light + dark, desktop + phone.
|
||||||
|
Exercise a rating click on the dev server (the JSON handler) to confirm the tint
|
||||||
|
switches. Run `cargo test --lib web::` and the ratings/issue router tests. Write
|
||||||
|
`handoff-rating.md`.
|
||||||
@@ -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