build: add Tailwind CSS v4 build pipeline with theme tokens

Replace the checked-in styles.css with a build-time Tailwind CSS
pipeline. input.css defines CSS custom properties for light/dark
themes, mapped to Tailwind utilities via @theme. build.rs runs
the Tailwind CLI to generate styles.css at compile time.
This commit is contained in:
Jon Seager 2026-02-05 17:02:13 +00:00
parent 6398adc853
commit 33c27ed0d0
No known key found for this signature in database
4 changed files with 210 additions and 36 deletions

3
.gitignore vendored
View file

@ -4,4 +4,5 @@ brewlog.db
TODO.md TODO.md
SPEC.md SPEC.md
backup.json backup.json
.env .env
templates/styles.css

66
build.rs Normal file
View file

@ -0,0 +1,66 @@
use std::process::Command;
fn main() {
git_hash();
tailwind();
}
fn git_hash() {
let output = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.unwrap_or_default();
let hash = output.trim();
println!("cargo:rustc-env=GIT_HASH={hash}");
println!("cargo:rerun-if-changed=.git/HEAD");
println!("cargo:rerun-if-changed=.git/refs/heads/");
}
fn tailwind() {
// Rerun when any template file changes (Tailwind scans them for classes)
for entry in walkdir("templates") {
println!("cargo:rerun-if-changed={entry}");
}
let mut cmd = Command::new("tailwindcss");
cmd.args(["-i", "templates/input.css", "-o", "templates/styles.css"]);
if std::env::var("PROFILE").as_deref() == Ok("release") {
cmd.arg("--minify");
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(status) => {
eprintln!("cargo:warning=tailwindcss exited with {status}");
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!("cargo:warning=tailwindcss not found on PATH, skipping CSS generation");
}
Err(e) => {
eprintln!("cargo:warning=failed to run tailwindcss: {e}");
}
}
}
fn walkdir(dir: &str) -> Vec<String> {
let mut files = Vec::new();
let mut stack = vec![std::path::PathBuf::from(dir)];
while let Some(path) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&path) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else {
files.push(path.display().to_string());
}
}
}
files
}

View file

@ -50,9 +50,13 @@
src = lib.cleanSource ./.; src = lib.cleanSource ./.;
cargoLock.lockFile = ./Cargo.lock; cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = [ pkgs.pkg-config ]; nativeBuildInputs = [ pkgs.pkg-config pkgs.tailwindcss_4 ];
buildInputs = [ pkgs.openssl ]; buildInputs = [ pkgs.openssl ];
preBuild = ''
tailwindcss -i templates/input.css -o templates/styles.css --minify
'';
meta = { meta = {
description = "Log your favourite roasters, roasts, brews and cafes!"; description = "Log your favourite roasters, roasts, brews and cafes!";
homepage = "https://github.com/jnsgruk/brewlog"; homepage = "https://github.com/jnsgruk/brewlog";
@ -79,6 +83,7 @@
nixfmt nixfmt
sqlite sqlite
sqlx-cli sqlx-cli
tailwindcss_4
]); ]);
}; };
}; };

View file

@ -1,7 +1,80 @@
@import "tailwindcss";
@source "./";
/* ── Dark mode variant (selector-based) ────────────────────────── */
@custom-variant dark (&:where([data-theme="dark"] *));
/* ── Design tokens ─────────────────────────────────────────────── */
:root { :root {
color-scheme: light; color-scheme: light;
/* Surfaces */
--page: #fafaf9; /* stone-50 */
--surface: #ffffff; /* white */
--surface-alt: #f5f5f4; /* stone-100 */
/* Borders */
--border: #e7e5e4; /* stone-200 */
--border-muted: #f5f5f4; /* stone-100 */
/* Accent */
--accent: #c2410c; /* orange-700 */
--accent-hover: #ea580c; /* orange-600 */
--accent-subtle: #fff7ed; /* orange-50 */
--accent-text: #ffffff;
/* Text */
--text: #1c1917; /* stone-900 */
--text-secondary: #78716c; /* stone-500 */
--text-muted: #a8a29e; /* stone-400 */
} }
[data-theme="dark"] {
color-scheme: dark;
--page: #1c1917;
--surface: #292524;
--surface-alt: #44403c;
--border: #57534e;
--border-muted: #44403c;
--accent: #ea580c;
--accent-hover: #f97316;
--accent-subtle: rgba(234, 88, 12, 0.1);
--accent-text: #ffffff;
--text: #e7e5e4;
--text-secondary: #a8a29e;
--text-muted: #78716c;
}
/* ── Theme: map raw vars to Tailwind utilities ─────────────────── */
@theme {
--color-page: var(--page);
--color-surface: var(--surface);
--color-surface-alt: var(--surface-alt);
--color-accent: var(--accent);
--color-accent-hover: var(--accent-hover);
--color-accent-subtle: var(--accent-subtle);
--color-accent-text: var(--accent-text);
}
/* ── Base: default border color ────────────────────────────────── */
@layer base {
*,
::after,
::before {
border-color: var(--border);
}
}
/* ── Typography ────────────────────────────────────────────────── */
body { body {
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
} }
@ -10,21 +83,23 @@ a {
text-decoration: none; text-decoration: none;
} }
/* ── Form controls ─────────────────────────────────────────────── */
.input-field { .input-field {
width: 100%; width: 100%;
min-width: 0; min-width: 0;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid rgba(120, 53, 15, 0.3); border: 1px solid var(--border);
background-color: rgba(254, 243, 199, 0.85); background-color: var(--surface);
padding: 0.6rem 0.75rem; padding: 0.6rem 0.75rem;
color: #4a2f0b; color: var(--text);
transition: border-color 150ms ease, box-shadow 150ms ease; transition: border-color 150ms ease, box-shadow 150ms ease;
} }
.input-field:focus { .input-field:focus {
outline: none; outline: none;
border-color: rgba(180, 83, 9, 0.6); border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(180, 83, 9, 0.2); box-shadow: 0 0 0 2px rgba(194, 65, 12, 0.15);
} }
.btn-adjust { .btn-adjust {
@ -34,18 +109,41 @@ a {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 0.375rem; border-radius: 0.375rem;
border: 1px solid rgba(245, 158, 11, 1); border: 1px solid var(--border);
background-color: transparent; background-color: transparent;
color: rgba(180, 83, 9, 1); color: var(--text-secondary);
font-weight: 700; font-weight: 700;
transition: background-color 150ms ease; transition: background-color 150ms ease, border-color 150ms ease;
} }
.btn-adjust:hover { .btn-adjust:hover {
background-color: rgba(255, 251, 235, 1); background-color: var(--surface-alt);
border-color: var(--accent);
color: var(--accent);
} }
/* Timeline layout */ /* ── Pills ─────────────────────────────────────────────────────── */
.pill {
display: inline-flex;
align-items: center;
border-radius: 9999px;
padding: 0.125rem 0.625rem;
font-size: 0.75rem;
line-height: 1rem;
font-weight: 600;
border: 1px solid transparent;
}
.pill-muted {
border-color: var(--border);
color: var(--text-secondary);
background-color: var(--surface-alt);
}
/* ── Timeline layout ───────────────────────────────────────────── */
/* Mobile: single column, centred line behind cards */ /* Mobile: single column, centred line behind cards */
.timeline-line { .timeline-line {
@ -68,7 +166,7 @@ a {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 10; z-index: 10;
background-color: #fefce8; /* amber-50, matches page background */ background-color: var(--page);
padding-top: 1rem; padding-top: 1rem;
padding-bottom: 1rem; padding-bottom: 1rem;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
@ -77,7 +175,7 @@ a {
} }
.timeline-heading.is-stuck { .timeline-heading.is-stuck {
border-bottom-color: #b45309; /* amber-700 */ border-bottom-color: var(--accent);
} }
.timeline-top-btn { .timeline-top-btn {
@ -87,7 +185,7 @@ a {
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
padding: 0.25rem; padding: 0.25rem;
color: #b45309; /* amber-700 */ color: var(--text-secondary);
opacity: 0.7; opacity: 0.7;
transition: opacity 0.15s ease; transition: opacity 0.15s ease;
} }
@ -100,6 +198,8 @@ a {
display: block; display: block;
} }
/* ── Data page ─────────────────────────────────────────────────── */
/* Hide duplicate search inside data page - search is in the tab bar */ /* Hide duplicate search inside data page - search is in the tab bar */
.data-page-content section:has(.responsive-table) > div:first-child:has(input[type="search"]) { .data-page-content section:has(.responsive-table) > div:first-child:has(input[type="search"]) {
display: none; display: none;
@ -110,7 +210,8 @@ a {
margin-top: 0; margin-top: 0;
} }
/* Responsive tables: convert rows to cards on mobile */ /* ── Responsive tables: convert rows to cards on mobile ────────── */
@media (max-width: 767px) { @media (max-width: 767px) {
/* Remove outer section border/background on mobile - cards are standalone */ /* Remove outer section border/background on mobile - cards are standalone */
section:has(.responsive-table) { section:has(.responsive-table) {
@ -156,16 +257,16 @@ a {
padding: 0; padding: 0;
} }
/* Match home page brew card style */ /* Mobile card style */
.responsive-table tr { .responsive-table tr {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
padding: 1rem; padding: 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid #fde68a !important; /* amber-200 */ border: 1px solid var(--border) !important;
background-color: #fffbeb; /* amber-50 */ background-color: var(--surface);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); /* shadow-sm */ box-shadow: none;
} }
.responsive-table td { .responsive-table td {
@ -184,7 +285,7 @@ a {
min-width: 5.5rem; min-width: 5.5rem;
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 500; font-weight: 500;
color: #78716c; /* stone-500 - more muted like brew card */ color: var(--text-secondary);
margin-right: 0.75rem; margin-right: 0.75rem;
} }
@ -202,14 +303,14 @@ a {
padding: 0 0 0.5rem 0; padding: 0 0 0.5rem 0;
padding-right: 2.5rem; padding-right: 2.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
border-bottom: 1px solid #fef3c7 !important; /* amber-100 */ border-bottom: 1px solid var(--border-muted) !important;
} }
.responsive-table td.card-title, .responsive-table td.card-title,
.responsive-table td.card-title > :first-child { .responsive-table td.card-title > :first-child {
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
color: #92400e; /* amber-800 */ color: var(--text);
} }
.responsive-table td.card-title::before { .responsive-table td.card-title::before {
@ -221,15 +322,15 @@ a {
order: 99; order: 99;
padding-top: 0.5rem; padding-top: 0.5rem;
margin-top: 0.5rem; margin-top: 0.5rem;
border-top: 1px solid #fef3c7 !important; /* amber-100 */ border-top: 1px solid var(--border-muted) !important;
font-size: 0.75rem; font-size: 0.75rem;
color: #78716c; color: var(--text-secondary);
} }
.responsive-table td.card-date::before { .responsive-table td.card-date::before {
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 500;
color: #78716c; color: var(--text-secondary);
min-width: auto; min-width: auto;
margin-right: 0.5rem; margin-right: 0.5rem;
} }
@ -262,13 +363,13 @@ a {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 0.375rem; border-radius: 0.375rem;
color: #78716c; color: var(--text-secondary);
transition: color 150ms ease, background-color 150ms ease; transition: color 150ms ease, background-color 150ms ease;
} }
.card-menu summary:hover { .card-menu summary:hover {
color: #b45309; color: var(--accent);
background-color: rgba(254, 243, 199, 0.8); background-color: var(--surface-alt);
} }
.card-menu summary::-webkit-details-marker { .card-menu summary::-webkit-details-marker {
@ -276,8 +377,8 @@ a {
} }
.card-menu[open] summary { .card-menu[open] summary {
color: #b45309; color: var(--accent);
background-color: rgba(254, 243, 199, 0.8); background-color: var(--surface-alt);
} }
.card-menu .card-menu-items { .card-menu .card-menu-items {
@ -288,8 +389,8 @@ a {
min-width: 10rem; min-width: 10rem;
margin-top: 0.25rem; margin-top: 0.25rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid rgba(217, 169, 55, 0.4); border: 1px solid var(--border);
background-color: #fffbeb; background-color: var(--surface);
padding: 0.25rem 0; padding: 0.25rem 0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
} }
@ -303,7 +404,7 @@ a {
gap: 0.5rem; gap: 0.5rem;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
font-size: 0.875rem; font-size: 0.875rem;
color: #44403c; color: var(--text);
text-align: left; text-align: left;
background: none; background: none;
border: none; border: none;
@ -315,7 +416,7 @@ a {
.card-menu .card-menu-items a:hover, .card-menu .card-menu-items a:hover,
.card-menu .card-menu-items button:hover, .card-menu .card-menu-items button:hover,
.card-menu .card-menu-items form button:hover { .card-menu .card-menu-items form button:hover {
background-color: rgba(254, 243, 199, 0.6); background-color: var(--surface-alt);
} }
.card-menu .card-menu-items .menu-item-danger { .card-menu .card-menu-items .menu-item-danger {
@ -331,7 +432,8 @@ a {
} }
} }
/* Desktop: alternating cards with central line */ /* ── Desktop timeline ──────────────────────────────────────────── */
@media (min-width: 768px) { @media (min-width: 768px) {
.timeline-list { .timeline-list {
display: flex; display: flex;