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
+2
View File
@@ -219,6 +219,7 @@ struct FullEntry {
href: String,
source: String,
reading_minutes: i64,
is_lead: bool,
summary: String,
why: Option<String>,
rating: Option<RatingWidget>,
@@ -351,6 +352,7 @@ pub async fn render_full(
href: article_href(date, pick.article.id),
source: pick.article.feed_title.clone(),
reading_minutes: pick.article.reading_minutes(),
is_lead: pick.is_lead,
summary: summary_for(&view.issue, pick)
.unwrap_or_default()
.to_string(),