refactor(css): add text-2xs and small-caps utilities, replace inline styles

- Add @utility text-2xs (0.65rem) and small-caps (font-variant) to input.css
- Replace inline style="font-variant: small-caps" with .small-caps class
  across all list partials and base.html footer
- Add gap support to .tab and .tab-mobile for icon+label layout
- Reduce mobile top padding (py-4 md:py-10) on main container
- Rename CSS section header from "Scrollbar hide" to "Utilities"
This commit is contained in:
Jon Seager 2026-02-07 22:36:28 +00:00
parent 0258ca6840
commit af9344202d
No known key found for this signature in database
9 changed files with 106 additions and 70 deletions

View file

@ -88,7 +88,7 @@ a {
text-decoration: none; text-decoration: none;
} }
/* ── Scrollbar hide ───────────────────────────────────────────── */ /* ── Utilities ────────────────────────────────────────────────── */
@utility scrollbar-hide { @utility scrollbar-hide {
-ms-overflow-style: none; -ms-overflow-style: none;
@ -98,6 +98,15 @@ a {
} }
} }
@utility text-2xs {
font-size: 0.65rem;
line-height: 1rem;
}
@utility small-caps {
font-variant: small-caps;
}
/* ── Form controls ─────────────────────────────────────────────── */ /* ── Form controls ─────────────────────────────────────────────── */
.input-field { .input-field {
@ -319,6 +328,7 @@ input.input-field[type="number"] {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0.375rem;
flex: 1; flex: 1;
border-radius: 0.375rem; border-radius: 0.375rem;
padding: 0.375rem 0.875rem; padding: 0.375rem 0.875rem;
@ -340,7 +350,9 @@ input.input-field[type="number"] {
} }
.tab-mobile { .tab-mobile {
display: block; display: flex;
align-items: center;
gap: 0.375rem;
width: 100%; width: 100%;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
font-size: 0.875rem; font-size: 0.875rem;

View file

@ -1,20 +1,25 @@
<!DOCTYPE html> <!doctype html>
<html lang="en" data-star-root> <html lang="en" data-star-root>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="{% block description %}Self-hosted coffee logging — track roasters, roasts, bags, brews, and gear.{% endblock %}"
/>
<title>{% block title %}Brewlog{% endblock %}</title> <title>{% block title %}Brewlog{% endblock %}</title>
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />
<link rel="icon" id="favicon" type="image/svg+xml" href="/favicon-light.svg" /> <link rel="icon" id="favicon" type="image/svg+xml" href="/favicon-light.svg" />
<script> <script>
(() => { ;(() => {
const stored = localStorage.getItem("theme"); const stored = localStorage.getItem("theme")
const isDark = stored === "dark" || (!stored && matchMedia("(prefers-color-scheme: dark)").matches); const isDark =
stored === "dark" || (!stored && matchMedia("(prefers-color-scheme: dark)").matches)
if (isDark) { if (isDark) {
document.documentElement.setAttribute("data-theme", "dark"); document.documentElement.setAttribute("data-theme", "dark")
document.getElementById("favicon").href = "/favicon-dark.svg"; document.getElementById("favicon").href = "/favicon-dark.svg"
} }
})(); })()
</script> </script>
<script <script
type="module" type="module"
@ -25,98 +30,117 @@
{% block head %}{% endblock %} {% block head %}{% endblock %}
<script> <script>
const toggleRow = (event) => { const toggleRow = (event) => {
if (event.target.closest('.card-detail')) return; if (event.target.closest(".card-detail")) return
const row = event.currentTarget; const row = event.currentTarget
row.classList.toggle('expanded'); row.classList.toggle("expanded")
row.querySelector('.card-detail')?.classList.toggle('hidden'); row.querySelector(".card-detail")?.classList.toggle("hidden")
const r = row.nextElementSibling; const r = row.nextElementSibling
if (r?.classList.contains('detail-row')) r.classList.toggle('hidden'); if (r?.classList.contains("detail-row")) r.classList.toggle("hidden")
row.querySelector('.icon-expand')?.classList.toggle('hidden'); row.querySelector(".icon-expand")?.classList.toggle("hidden")
row.querySelector('.icon-collapse')?.classList.toggle('hidden'); row.querySelector(".icon-collapse")?.classList.toggle("hidden")
}; }
</script> </script>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const mobileQuery = window.matchMedia("(max-width: 767px)"); const mobileQuery = window.matchMedia("(max-width: 767px)")
if (!mobileQuery.matches) return; if (!mobileQuery.matches) return
const setupInfiniteScroll = () => { const setupInfiniteScroll = () => {
document.querySelectorAll("[data-infinite-scroll]").forEach((container) => { document.querySelectorAll("[data-infinite-scroll]").forEach((container) => {
const sentinel = container.querySelector(".infinite-scroll-sentinel"); const sentinel = container.querySelector(".infinite-scroll-sentinel")
if (!sentinel || sentinel.dataset.observed) return; if (!sentinel || sentinel.dataset.observed) return
sentinel.dataset.observed = "true"; sentinel.dataset.observed = "true"
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (!entry.isIntersecting) return; if (!entry.isIntersecting) return
const nextUrl = container.getAttribute("data-next-url"); const nextUrl = container.getAttribute("data-next-url")
const targetSelector = container.getAttribute("data-target"); const targetSelector = container.getAttribute("data-target")
if (!nextUrl || !targetSelector) { if (!nextUrl || !targetSelector) {
observer.disconnect(); observer.disconnect()
return; return
} }
observer.disconnect(); observer.disconnect()
sentinel.remove(); sentinel.remove()
fetch(nextUrl, { fetch(nextUrl, {
headers: { "datastar-request": "true" }, headers: { "datastar-request": "true" },
}) })
.then((res) => res.text()) .then((res) => res.text())
.then((html) => { .then((html) => {
const doc = new DOMParser().parseFromString(html, "text/html"); const doc = new DOMParser().parseFromString(html, "text/html")
const newTarget = doc.querySelector(targetSelector); const newTarget = doc.querySelector(targetSelector)
if (!newTarget) return; if (!newTarget) return
// Append new rows to existing tbody // Append new rows to existing tbody
const existingTbody = container.querySelector("tbody"); const existingTbody = container.querySelector("tbody")
const newTbody = newTarget.querySelector("tbody"); const newTbody = newTarget.querySelector("tbody")
if (existingTbody && newTbody) { if (existingTbody && newTbody) {
Array.from(newTbody.children).forEach((row) => { Array.from(newTbody.children).forEach((row) => {
existingTbody.appendChild(document.adoptNode(row)); existingTbody.appendChild(document.adoptNode(row))
}); })
} }
// Check if there are more pages // Check if there are more pages
const newContainer = newTarget.querySelector("[data-infinite-scroll]"); const newContainer = newTarget.querySelector("[data-infinite-scroll]")
const newNextUrl = newContainer && newContainer.getAttribute("data-next-url"); const newNextUrl = newContainer && newContainer.getAttribute("data-next-url")
if (newNextUrl) { if (newNextUrl) {
container.setAttribute("data-next-url", newNextUrl); container.setAttribute("data-next-url", newNextUrl)
const newSentinel = document.createElement("div"); const newSentinel = document.createElement("div")
newSentinel.className = "infinite-scroll-sentinel h-4 md:hidden"; newSentinel.className = "infinite-scroll-sentinel h-4 md:hidden"
newSentinel.setAttribute("aria-hidden", "true"); newSentinel.setAttribute("aria-hidden", "true")
container.appendChild(newSentinel); container.appendChild(newSentinel)
setupInfiniteScroll(); setupInfiniteScroll()
} else { } else {
container.removeAttribute("data-next-url"); container.removeAttribute("data-next-url")
container.removeAttribute("data-infinite-scroll"); container.removeAttribute("data-infinite-scroll")
} }
}); })
}); })
}, },
{ rootMargin: "200px" } { rootMargin: "200px" }
); )
observer.observe(sentinel); observer.observe(sentinel)
}); })
}; }
setupInfiniteScroll(); setupInfiniteScroll()
const bodyObserver = new MutationObserver(() => { const bodyObserver = new MutationObserver(() => {
setupInfiniteScroll(); setupInfiniteScroll()
}); })
bodyObserver.observe(document.body, { childList: true, subtree: true }); bodyObserver.observe(document.body, { childList: true, subtree: true })
}); })
</script> </script>
</head> </head>
<body class="min-h-screen bg-page text-text"> <body class="min-h-screen bg-page text-text">
<main class="mx-auto flex w-full max-w-5xl flex-col gap-8 px-6 py-10"> <main class="mx-auto flex w-full max-w-5xl flex-col gap-8 px-6 py-4 md:py-10">
{% include "partials/nav.html" %} {% block content %}{% endblock %} {% include "partials/nav.html" %} {% block content %}{% endblock %}
<footer class="mt-8 text-center text-xs tracking-widest text-text-muted" style="font-variant: small-caps"> <footer
<p>B{rew}log by <a href="https://jnsgr.uk" class="text-text-muted hover:text-accent transition" target="_blank" rel="noreferrer noopener">jnsgruk</a> · <a href="https://github.com/jnsgruk/brewlog/commit/{{ version_info.commit }}" class="text-text-muted hover:text-accent transition" target="_blank" rel="noreferrer noopener">{{ version_info.commit }}</a></p> class="mt-8 text-center text-xs tracking-widest text-text-muted small-caps"
>
<p>
B{rew}log by
<a
href="https://jnsgr.uk"
class="text-text-muted hover:text-accent transition"
target="_blank"
rel="noreferrer noopener"
>jnsgruk</a
>
·
<a
href="https://github.com/jnsgruk/brewlog/commit/{{ version_info.commit }}"
class="text-text-muted hover:text-accent transition"
target="_blank"
rel="noreferrer noopener"
>{{ version_info.commit }}</a
>
</p>
</footer> </footer>
</main> </main>
</body> </body>

View file

@ -21,7 +21,7 @@
{{ table::search_header(navigator, "#bag-list") }} {{ table::search_header(navigator, "#bag-list") }}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text"> <table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text" style="font-variant: small-caps"> <thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text small-caps">
<tr> <tr>
{{ table::sortable_header("Added", "created-at", navigator, "#bag-list") }} {{ table::sortable_header("Added", "created-at", navigator, "#bag-list") }}
{{ table::sortable_header("Roaster", "roaster", navigator, "#bag-list") }} {{ table::sortable_header("Roaster", "roaster", navigator, "#bag-list") }}
@ -56,7 +56,7 @@
<div class="h-1.5 rounded-full bg-surface-alt overflow-hidden"> <div class="h-1.5 rounded-full bg-surface-alt overflow-hidden">
<div class="h-full rounded-full bg-accent" style="width: {{ bag.used_percent }}%"></div> <div class="h-full rounded-full bg-accent" style="width: {{ bag.used_percent }}%"></div>
</div> </div>
<div class="mt-0.5 text-text-muted" style="font-variant: small-caps; font-size: 0.7rem">{{ bag.remaining }} / {{ bag.amount }}g</div> <div class="mt-0.5 text-text-muted small-caps" style="font-size: 0.7rem">{{ bag.remaining }} / {{ bag.amount }}g</div>
</div> </div>
{% endif %} {% endif %}
</td> </td>
@ -65,7 +65,7 @@
<div class="h-1.5 rounded-full bg-surface-alt overflow-hidden"> <div class="h-1.5 rounded-full bg-surface-alt overflow-hidden">
<div class="h-full rounded-full bg-accent" style="width: {{ bag.used_percent }}%"></div> <div class="h-full rounded-full bg-accent" style="width: {{ bag.used_percent }}%"></div>
</div> </div>
<div class="mt-0.5 text-right text-text-muted" style="font-variant: small-caps; font-size: 0.7rem">{{ bag.remaining }} / {{ bag.amount }}g</div> <div class="mt-0.5 text-right text-text-muted small-caps" style="font-size: 0.7rem">{{ bag.remaining }} / {{ bag.amount }}g</div>
</td> </td>
{% endif %} {% endif %}
<td data-label="Finished" class="mobile-hidden whitespace-nowrap px-4 py-3 font-medium text-text-secondary"> <td data-label="Finished" class="mobile-hidden whitespace-nowrap px-4 py-3 font-medium text-text-secondary">

View file

@ -36,7 +36,7 @@
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text"> <table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text" style="font-variant: small-caps"> <thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text small-caps">
<tr> <tr>
{{ table::sortable_header("Added", "created-at", navigator, "#brew-list") }} {{ table::sortable_header("Added", "created-at", navigator, "#brew-list") }}
<th scope="col" class="px-4 py-3">Coffee</th> <th scope="col" class="px-4 py-3">Coffee</th>

View file

@ -21,7 +21,7 @@
{{ table::search_header(navigator, "#cafe-list") }} {{ table::search_header(navigator, "#cafe-list") }}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text"> <table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text" style="font-variant: small-caps"> <thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text small-caps">
<tr> <tr>
{{ table::sortable_header("Added", "created-at", navigator, "#cafe-list") }} {{ table::sortable_header("Added", "created-at", navigator, "#cafe-list") }}
{{ table::sortable_header("Name", "name", navigator, "#cafe-list") }} {{ table::sortable_header("Name", "name", navigator, "#cafe-list") }}

View file

@ -21,7 +21,7 @@
{{ table::search_header(navigator, "#cup-list") }} {{ table::search_header(navigator, "#cup-list") }}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text"> <table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text" style="font-variant: small-caps"> <thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text small-caps">
<tr> <tr>
{{ table::sortable_header("Added", "created-at", navigator, "#cup-list") }} {{ table::sortable_header("Added", "created-at", navigator, "#cup-list") }}
{{ table::sortable_header("Roast", "roast", navigator, "#cup-list") }} {{ table::sortable_header("Roast", "roast", navigator, "#cup-list") }}

View file

@ -22,7 +22,7 @@
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text"> <table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text" style="font-variant: small-caps"> <thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text small-caps">
<tr> <tr>
{{ table::sortable_header("Added", "created-at", navigator, "#gear-list") }} {{ table::sortable_header("Added", "created-at", navigator, "#gear-list") }}
{{ table::sortable_header("Category", "category", navigator, "#gear-list") }} {{ table::sortable_header("Category", "category", navigator, "#gear-list") }}

View file

@ -21,7 +21,7 @@
{{ table::search_header(navigator, "#roast-list") }} {{ table::search_header(navigator, "#roast-list") }}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text"> <table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text" style="font-variant: small-caps"> <thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text small-caps">
<tr> <tr>
{{ table::sortable_header("Added", "created-at", navigator, "#roast-list") }} {{ table::sortable_header("Added", "created-at", navigator, "#roast-list") }}
{{ table::sortable_header("Roast", "name", navigator, "#roast-list") }} {{ table::sortable_header("Roast", "name", navigator, "#roast-list") }}

View file

@ -21,7 +21,7 @@
{{ table::search_header(navigator, "#roaster-list") }} {{ table::search_header(navigator, "#roaster-list") }}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text"> <table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text" style="font-variant: small-caps"> <thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text small-caps">
<tr> <tr>
{{ table::sortable_header("Added", "created-at", navigator, "#roaster-list") }} {{ table::sortable_header("Added", "created-at", navigator, "#roaster-list") }}
{{ table::sortable_header("Name", "name", navigator, "#roaster-list") }} {{ table::sortable_header("Name", "name", navigator, "#roaster-list") }}