Web dashboard v2 step 2: Tailwind design system, theme toggle, reader pages

Replaces the hand-written stylesheet with a Tailwind CSS v4 source
(src/web/tailwind.css) whose minified build is committed as
src/web/static/app.css and still embedded with include_str!, so the binary
and the test suite never need Node. package.json pins tailwindcss and
@tailwindcss/cli to 4.3.3 and adds css, css:watch and css:check.

Colours are semantic CSS variables swapped per theme and exposed to Tailwind
through @theme inline, so templates write bg-paper/text-ink/border-rule once
and both themes work. theme.js runs synchronously in <head> to set
data-theme before paint; app.js cycles the toggle system -> light -> dark and
persists the choice. Newsreader is self-hosted and served from
web::static_asset with the same ETag/304 logic as the other assets.

Every reader template is restyled: ears row, centred masthead with its rules,
tracked section labels, the drop cap on The Brief, "Why it's here" with its
accent bar, the segmented rating control, prev/next cards, and the colophon
as a two-column definition grid. Dashboard templates are untouched (step 3).

Also fixes a .block collision with Tailwind's display utility, the SVG
`hidden` property that never toggled the theme icon, and points the dev
seed's profile_path at ./dev so previewing cannot rewrite data/profile.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NHyYupFdBiR4VfoUM7NjSM
This commit is contained in:
2026-09-03 20:51:08 +00:00
co-authored by Claude Fable 5.1
parent 61ed37fc20
commit 12134f3596
27 changed files with 874 additions and 248 deletions
@@ -0,0 +1,151 @@
# Step 2 handoff — Tailwind design system, theme toggle, reader pages
Branch `v2-frontend`, worktree `/home/thallada/workspace/the-daily-epub-v2b`, one commit.
Step 3 (dashboard restyle) and step 4 (TOC sidebar) build on what is described below.
## Landed
### Toolchain
- `package.json` (root, `private`) pins `tailwindcss` and `@tailwindcss/cli` to `4.3.3` with
scripts `css`, `css:watch` and `css:check`. `package-lock.json` is committed.
`.gitignore` already carried `node_modules/` and `dev/`.
- `src/web/tailwind.css` is the single source; `src/web/static/app.css` is the **committed,
minified build** and is still embedded with `include_str!`, so `cargo build`, `cargo test`
and the deployed binary never need Node.
- `npm run css:check` rebuilds to a temp file and `diff -u`s it against the committed
`app.css`, exiting non-zero on drift (verified in both directions).
- Workflow documented in `README.md` and `docs/runbooks/web-dashboard-rollout.md`
(`npm ci && npm run css:check` added to the rollout build step).
### Assets
- `web::static_asset` gained fixed match arms (no path joins) for `theme.js`,
`Newsreader.woff2` and `Newsreader-italic.woff2`. The asset tuple is now
`(&str, &'static [u8])` so text assets use `include_str!(...).as_bytes()` and the fonts use
`include_bytes!`; the SHA-256 ETag / 304 / long-cache path is unchanged and its test still
passes. URLs are flat: `/static/Newsreader.woff2`, not `/static/fonts/…`.
- `@font-face` for both faces (`font-weight: 200 800`, `font-stretch: normal`,
`font-display: swap`, Google's latin `unicode-range`), plus a
`<link rel="preload" as="font" type="font/woff2" crossorigin>` for the regular face.
`document.fonts.check('600 48px Newsreader')` is true on a loaded page and the server log
shows no 404s.
### Theme
- `src/web/static/theme.js` runs synchronously in `<head>` (external file, CSP-safe): reads
`localStorage.theme`, sets or removes `document.documentElement.dataset.theme` before paint.
- `app.js` drives `[data-theme-toggle]`: cycles **system → light → dark → system**, persists
(`removeItem` for system), updates the attribute, the SVG icon and the visible label, keeps
`aria-label="Theme: <state>"`, and re-renders on `matchMedia` changes while in system mode.
- Colours are semantic CSS variables on `:root`, duplicated into
`@media (prefers-color-scheme: dark) :root:not([data-theme="light"])` and
`:root[data-theme="dark"]`, exposed to Tailwind via `@theme inline`, with
`@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *))`.
`color-scheme` follows the theme.
### Reader templates
`layout.html` (ears row + centred masthead with the 1px rule above and the 3px double rule
below + primary nav with `aria-current="page"` + admin nav row + flash + `<main id="content">`
+ footer + skip link), `issue_public.html`, `issue_full.html`, `issue_list.html`,
`article.html`, `world.html`, `behind.html`, `login.html`, `account.html`, `error.html` and
`_rating_widget.html` are all restyled with utilities. Every hook listed in the shared brief
(`form.rating`, `button[data-label]`, `.active`, `aria-pressed`, `form[data-confirm]`,
`details[id]`, `table[data-filter]`, `.table-filter`, `[data-refresh]`, `.scroll-x`, ids,
names, actions) is intact.
## What step 3 can use
Tokens (`@theme inline`, so both a CSS variable and a Tailwind colour utility exist):
| token | utilities |
| --- | --- |
| `--paper`, `--paper-2` | `bg-paper`, `bg-paper-2` |
| `--ink`, `--ink-2`, `--muted` | `text-ink`, `text-ink-2`, `text-muted`, `bg-ink` |
| `--rule`, `--rule-strong` | `border-rule`, `border-rule-strong` |
| `--accent`, `--accent-hover` | `text-accent`, `border-accent`, `bg-accent`, `hover:text-accent-hover` |
| `--loved`, `--good`, `--down`, `--warn` | `text-loved`, `text-good`, `text-down`, `text-warn` |
| `--font-serif` / `--font-sans` / `--font-mono` | `font-serif`, `font-sans`, `font-mono` |
Component classes in `@layer components`: `.btn`, `.btn-primary`, `.badge` + state modifiers
(`selected loved good down excluded shortlisted admitted eligible cleared assessed triaged
reason running requested ok degraded failed dry_run`), `.kv`, `.scroll-x`, `.prose-body`,
`.rating`/`.rating-prompt`/`.rating-note`, `.flash`, `.error`, `.notice`, `.dashboard`,
`.cards`/`.card`, `.muted`/`.meta`/`.num`, `.filters`, `.table-filter`, `.pager`/`.run-nav`/
`.tabs`, `pre.block`/`pre.preview`/`pre.journal`, `.funnel`, `.spark` (+ `.s0``.s5`, `.line`).
`@layer base` styles `table`/`thead`/`th`/`td` (sticky head on `bg-paper`, hairline rules,
`hover:bg-paper-2`, tabular numerals) and all form controls.
Repeated inline recipes step 3 will probably want: small label
`font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted`; headline
`font-semibold leading-[1.1] tracking-[-0.01em]`; reader measure
`mx-auto max-w-[68ch] px-4 sm:px-6`; dashboard shell `.dashboard` (`max-w-7xl`, sans).
## Deviations and fixes beyond the brief
- **`.block` collided with Tailwind's `block` display utility.** The legacy component was
renamed to `pre.block`, which keeps both `<pre class="block">` dashboard usages working and
frees `class="block"` for real layout. Before this, the article prev/next cards rendered as
grey monospace boxes.
- **`icon.hidden = …` does not work on SVG elements** (`hidden` is an `HTMLElement` IDL
attribute); the theme icon never changed. `app.js` now uses
`icon.toggleAttribute("hidden", …)`. `[hidden] { display:none !important }` stays in
`@layer base` so it beats display utilities.
- `.prose-body` lists had no markers after Preflight; added `list-disc` / `list-decimal` and a
muted `::marker`.
- The flash bar was flush to the viewport edge while the header used a gutter; `.flash` lost
its own `mx-auto max-w-7xl` and the layout wraps it in `mx-auto max-w-7xl px-4 sm:px-6`.
- The article prev/next grid rendered an empty first cell when there is no previous article;
the placeholder `<span>` is gone and the next card takes `sm:col-start-2`.
- `examples/seed_dev_db.rs` now writes `profile_path` into the throwaway `./dev` directory and
seeds it from `data/profile.md`. Without this, saving on `/dashboard/profile` while running
the dev server **rewrites the repository's `data/profile.md`** and breaks
`curate::profile::tests::shipped_profile_and_opml_parse`. It also fixes a pre-existing
`clippy::manual_is_multiple_of` error in that example (`cargo clippy --all-targets
-D warnings` did not pass on this branch before).
- `Page::new` for the Jobs and Users dashboard pages now passes `"jobs"` / `"users"` instead of
`"dashboard"`, so the admin nav highlights the right entry.
- `FullEntry` gained `is_lead` so the full issue's lead story is set larger (`text-3xl` vs
`text-2xl`), matching what `issue_public.html` already did.
- Fonts are served flat from `/static/<name>.woff2` (the brief allowed either shape).
- `issue_public.html` carries `{# step 1 adds summary/why here #}` at the point where the
backend step's summary/`why` markup should go; use the same treatment as `issue_full.html`
(summary as a body-size serif paragraph, then `border-l-2 border-accent pl-3 italic`).
- The dashboard templates are deliberately unstyled beyond what the base/component layers give
them. They render without errors (all nine routes smoke-tested) but look plain — step 3.
- The archive month label is still the raw `YYYY-MM` produced by `web::public`; changing it is
a backend concern and was left alone.
## Verification
- `npm run css` / `npm run css:check` — clean; drift is detected (exit 1) when `app.css` is
edited by hand.
- `cargo fmt`, `cargo clippy --all-targets -- -D warnings` — clean.
- `cargo test`**475 passed, 0 failed** (439 lib + 9 + 2 + 3 + 4 + 7 + 9 + 2 integration,
one binary with 0 tests). Run outside the Codex sandbox, so the socket-binding tests ran too.
No test needed a cosmetic-class update.
- Seeded dev server on `127.0.0.1:3599`. All of `/`, `/issues`, `/issues/2026-09-01` (legacy),
`/issues/2026-09-02`, `/issues/2026-09-02/articles/1`, `/…/world`, `/…/behind`, `/account`,
`/feed.xml` and the nine `/dashboard/*` routes return 200 with **no console errors and no
asset 404s**; `/issues/1999-01-01` returns the styled 404 page.
- Theme toggle asserted with Playwright: system → light → dark → system, `localStorage.theme`
written/removed, `data-theme` set/removed, icon and label follow, state survives a reload.
- Focus order checked: the skip link is the first tab stop and becomes visible; the toggle
shows the accent focus ring.
### Screenshots
`/tmp/claude-1000/-home-thallada-workspace-the-daily-epub/e963db53-510f-4312-ac87-460291a92781/scratchpad/step2-shots/`
— 83 PNGs. `/`, `/issues`, `/issues/2026-09-02`, `/issues/2026-09-02/articles/1`, `/…/world`,
`/…/behind`, `/account` and `/login` × {anonymous, reader, admin} × {light, dark} ×
{1280px desktop, 390px mobile}, full page. Named
`<path>-<user>-<theme>[-mobile].png`. Plus `toggle-1..4.png` (the toggle cycle),
`flash-light/dark.png`, `rating-active-light.png`, `focus-skiplink.png`, `focus-toggle.png`
and `issues_1999-01-01-none-{light,dark}.png` (404 page).
## Left for later
- Step 1 adds `summary`/`why` to the public entries (marker comment in place).
- Step 3 restyles `src/web/templates/dashboard/*` on top of this system.
- Step 4 adds the collapsing table-of-contents sidebar to the signed-in issue pages.
+8 -1
View File
@@ -6,7 +6,15 @@ already be complete.
## 1. Build and install
The deployed binary does not require Node. For UI development, edit
`src/web/tailwind.css`, the templates, or `src/web/static/app.js`, then run
`npm run css` and commit the regenerated `src/web/static/app.css` alongside the
source. CI and release preparation should run `npm run css:check` to detect
stylesheet drift.
```sh
npm ci
npm run css:check
cargo test
cargo build --release
sudo install -m0755 target/release/daily-epub /usr/local/bin/daily-epub
@@ -134,4 +142,3 @@ Candidate and assessment history is pruned after
`curation.ranking.telemetry_retention_days` (180 by default). Raise it now if the dashboard
should retain a longer article/run history, then reload `/dashboard/settings` and verify the
effective value. This changes future pruning only; it cannot restore rows already deleted.