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:
@@ -1,3 +1,44 @@
|
||||
const themeToggle = document.querySelector("[data-theme-toggle]");
|
||||
if (themeToggle) {
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const readTheme = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem("theme");
|
||||
return saved === "light" || saved === "dark" ? saved : "system";
|
||||
} catch (_) {
|
||||
return "system";
|
||||
}
|
||||
};
|
||||
const renderTheme = (theme) => {
|
||||
themeToggle.setAttribute("aria-label", `Theme: ${theme}`);
|
||||
themeToggle.querySelector("[data-theme-label]").textContent = theme[0].toUpperCase() + theme.slice(1);
|
||||
themeToggle.querySelectorAll("[data-theme-icon]").forEach((icon) => {
|
||||
// SVGElement has no `hidden` IDL attribute; toggle the content attribute.
|
||||
icon.toggleAttribute("hidden", icon.getAttribute("data-theme-icon") !== theme);
|
||||
});
|
||||
};
|
||||
const setTheme = (theme) => {
|
||||
if (theme === "system") document.documentElement.removeAttribute("data-theme");
|
||||
else document.documentElement.dataset.theme = theme;
|
||||
try {
|
||||
if (theme === "system") localStorage.removeItem("theme");
|
||||
else localStorage.setItem("theme", theme);
|
||||
} catch (_) {}
|
||||
renderTheme(theme);
|
||||
};
|
||||
renderTheme(readTheme());
|
||||
themeToggle.addEventListener("click", () => {
|
||||
const current = readTheme();
|
||||
setTheme(current === "system" ? "light" : current === "light" ? "dark" : "system");
|
||||
});
|
||||
media.addEventListener("change", () => {
|
||||
if (readTheme() === "system") {
|
||||
document.documentElement.removeAttribute("data-theme");
|
||||
renderTheme("system");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("submit", (event) => {
|
||||
const form = event.target;
|
||||
if (form.matches("form.rating") && event.submitter) {
|
||||
|
||||
Reference in New Issue
Block a user