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:
parent
6398adc853
commit
33c27ed0d0
4 changed files with 210 additions and 36 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,3 +5,4 @@ TODO.md
|
||||||
SPEC.md
|
SPEC.md
|
||||||
backup.json
|
backup.json
|
||||||
.env
|
.env
|
||||||
|
templates/styles.css
|
||||||
66
build.rs
Normal file
66
build.rs
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
Loading…
Reference in a new issue