feat(ui): enhance desktop tables and mobile card views

- Add spacious flat table CSS for desktop (transparent card wrapper,
  lighter dividers, visible hover, larger cell padding)
- Split roaster/cafe columns into separate Country and City columns
  with sortable headers
- Split cup Roast/Roaster into separate desktop columns with city
  sort support (full-stack: domain, repo, view, template)
- Add sortable Status column to bags with colored pills (green Open,
  amber Closed) and remaining weight subtext
- Style bag Finished column as date-only (NaiveDate) and Weight
  column with smaller text
- Show status pill next to mobile bag card actions menu
- Add city to cups mobile card view
- Use middle dot separator for brew recipe weight/volume
- Add tasting notes pills to roast list, brew notes to brew list
- Fix build.rs to use rerun-if-changed for CSS and templates
This commit is contained in:
Jon Seager 2026-02-06 15:40:40 +00:00
parent c2cbbd58d7
commit 3a960b9d14
No known key found for this signature in database
15 changed files with 153 additions and 63 deletions

View file

@ -1,3 +1,4 @@
use std::path::Path;
use std::process::Command;
fn main() {
@ -18,6 +19,9 @@ fn git_hash() {
}
fn tailwind() {
println!("cargo:rerun-if-changed=static/css/input.css");
println!("cargo:rerun-if-changed=templates/");
let mut cmd = Command::new("tailwindcss");
cmd.args(["-i", "static/css/input.css", "-o", "static/css/styles.css"]);
@ -25,6 +29,8 @@ fn tailwind() {
cmd.arg("--minify");
}
let output = Path::new("static/css/styles.css");
match cmd.status() {
Ok(status) if status.success() => {}
Ok(status) => {
@ -37,4 +43,9 @@ fn tailwind() {
eprintln!("cargo:warning=failed to run tailwindcss: {e}");
}
}
// Ensure the file exists so include_str!() doesn't break the build
if !output.exists() {
std::fs::write(output, "/* tailwindcss not available */").ok();
}
}

View file

@ -89,6 +89,7 @@ pub enum BagSortKey {
UpdatedAt,
Roaster,
Roast,
Status,
FinishedAt,
}
@ -104,6 +105,7 @@ impl SortKey for BagSortKey {
"updated-at" => Some(BagSortKey::UpdatedAt),
"roaster" => Some(BagSortKey::Roaster),
"roast" => Some(BagSortKey::Roast),
"status" => Some(BagSortKey::Status),
"finished-at" => Some(BagSortKey::FinishedAt),
_ => None,
}
@ -116,13 +118,14 @@ impl SortKey for BagSortKey {
BagSortKey::UpdatedAt => "updated-at",
BagSortKey::Roaster => "roaster",
BagSortKey::Roast => "roast",
BagSortKey::Status => "status",
BagSortKey::FinishedAt => "finished-at",
}
}
fn default_direction(self) -> SortDirection {
match self {
BagSortKey::Roaster | BagSortKey::Roast => SortDirection::Asc,
BagSortKey::Roaster | BagSortKey::Roast | BagSortKey::Status => SortDirection::Asc,
_ => SortDirection::Desc,
}
}

View file

@ -24,6 +24,7 @@ pub struct CupWithDetails {
pub roaster_slug: String,
pub cafe_name: String,
pub cafe_slug: String,
pub cafe_city: String,
}
impl CupWithDetails {
@ -96,7 +97,9 @@ impl CupFilter {
pub enum CupSortKey {
CreatedAt,
CafeName,
CafeCity,
RoastName,
RoasterName,
}
impl SortKey for CupSortKey {
@ -108,7 +111,9 @@ impl SortKey for CupSortKey {
match value {
"created-at" => Some(CupSortKey::CreatedAt),
"cafe" => Some(CupSortKey::CafeName),
"city" => Some(CupSortKey::CafeCity),
"roast" => Some(CupSortKey::RoastName),
"roaster" => Some(CupSortKey::RoasterName),
_ => None,
}
}
@ -117,7 +122,9 @@ impl SortKey for CupSortKey {
match self {
CupSortKey::CreatedAt => "created-at",
CupSortKey::CafeName => "cafe",
CupSortKey::CafeCity => "city",
CupSortKey::RoastName => "roast",
CupSortKey::RoasterName => "roaster",
}
}

View file

@ -42,6 +42,7 @@ impl SqlBagRepository {
BagSortKey::UpdatedAt => format!("b.updated_at {dir_sql}, b.id DESC"),
BagSortKey::Roaster => format!("LOWER(rr.name) {dir_sql}, b.created_at DESC"),
BagSortKey::Roast => format!("LOWER(r.name) {dir_sql}, b.created_at DESC"),
BagSortKey::Status => format!("b.closed {dir_sql}, b.created_at DESC"),
BagSortKey::FinishedAt => format!("b.finished_at {dir_sql}, b.created_at DESC"),
}
}

View file

@ -15,7 +15,8 @@ const BASE_SELECT: &str = r"
c.created_at, c.updated_at,
r.name as roast_name, r.slug as roast_slug,
rr.name as roaster_name, rr.slug as roaster_slug,
ca.name as cafe_name, ca.slug as cafe_slug
ca.name as cafe_name, ca.slug as cafe_slug,
ca.city as cafe_city
FROM cups c
JOIN roasts r ON c.roast_id = r.id
JOIN roasters rr ON r.roaster_id = rr.id
@ -43,9 +44,15 @@ impl SqlCupRepository {
CupSortKey::CafeName => {
format!("LOWER(ca.name) {dir_sql}, c.created_at DESC")
}
CupSortKey::CafeCity => {
format!("LOWER(ca.city) {dir_sql}, c.created_at DESC")
}
CupSortKey::RoastName => {
format!("LOWER(r.name) {dir_sql}, c.created_at DESC")
}
CupSortKey::RoasterName => {
format!("LOWER(rr.name) {dir_sql}, c.created_at DESC")
}
}
}
@ -74,6 +81,7 @@ impl SqlCupRepository {
roaster_slug: record.roaster_slug,
cafe_name: record.cafe_name,
cafe_slug: record.cafe_slug,
cafe_city: record.cafe_city,
}
}
@ -219,4 +227,5 @@ struct CupWithDetailsRecord {
roaster_slug: String,
cafe_name: String,
cafe_slug: String,
cafe_city: String,
}

View file

@ -8,7 +8,7 @@ pub struct BagView {
pub amount: String,
pub remaining: String,
pub closed: bool,
pub finished_at: String,
pub finished_date: String,
pub created_date: String,
pub created_time: String,
pub roast_name: String,
@ -26,10 +26,10 @@ impl BagView {
amount: format!("{:.1}", bag.bag.amount),
remaining: format!("{:.1}", bag.bag.remaining),
closed: bag.bag.closed,
finished_at: bag
finished_date: bag
.bag
.finished_at
.map_or_else(|| "".to_string(), |d| d.to_string()),
.map_or_else(|| "".to_string(), |d| d.format("%Y-%m-%d").to_string()),
created_date: bag.bag.created_at.format("%Y-%m-%d").to_string(),
created_time: bag.bag.created_at.format("%H:%M").to_string(),
roast_name: bag.roast_name,

View file

@ -9,6 +9,7 @@ pub struct CupView {
pub roaster_slug: String,
pub cafe_name: String,
pub cafe_slug: String,
pub cafe_city: String,
pub created_date: String,
pub created_time: String,
}
@ -23,6 +24,7 @@ impl CupView {
roaster_slug: cup.roaster_slug,
cafe_name: cup.cafe_name,
cafe_slug: cup.cafe_slug,
cafe_city: cup.cafe_city,
created_date: cup.cup.created_at.format("%Y-%m-%d").to_string(),
created_time: cup.cup.created_at.format("%H:%M").to_string(),
}

View file

@ -548,14 +548,44 @@ a {
border-bottom-width: 1px;
}
/* Hide Actions column on desktop — whole row is clickable */
.responsive-table .actions-col,
.responsive-table td.card-actions {
display: none;
}
/* Pointer cursor on clickable rows */
.responsive-table tbody > tr[onclick] {
cursor: pointer;
}
/* Hover: accent tint + left bar */
.responsive-table tbody > tr:not(.detail-row):hover {
background-color: var(--accent-subtle);
box-shadow: inset 3px 0 0 var(--accent);
}
/* Expanded row: persistent accent highlight */
.responsive-table tbody > tr.expanded {
background-color: var(--accent-subtle);
box-shadow: inset 3px 0 0 var(--accent);
}
/* Expanded row: remove bottom border so detail row merges */
.responsive-table tbody > tr.expanded {
border-bottom-width: 0;
}
/* Detail row: light grey bg + left accent bar, no top border */
.responsive-table tbody > tr.expanded + tr.detail-row {
background-color: var(--surface-alt);
box-shadow: inset 3px 0 0 var(--accent);
border-top-width: 0;
}
/* Detail row: keep compact */
.responsive-table .detail-row td {
background-color: transparent;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}

View file

@ -27,16 +27,18 @@
{% call table::sortable_header("Roaster", "roaster", navigator, "#bag-list") %}
{% call table::sortable_header("Roast", "roast", navigator, "#bag-list") %}
<th scope="col" class="px-4 py-3">Weight</th>
<th scope="col" class="px-4 py-3">Status</th>
{% call table::sortable_header("Status", "status", navigator, "#bag-list") %}
{% call table::sortable_header("Finished", "finished-at", navigator, "#bag-list") %}
{% if is_authenticated %}
<th scope="col" class="px-4 py-3 text-right">Actions</th>
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
{% endif %}
</tr>
</thead>
<tbody class="divide-y/70">
{% for bag in bags.items %}
<tr class="transition hover:bg-surface-alt">
<tr class="transition hover:bg-surface-alt"
{% if is_authenticated %}onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"{% endif %}
>
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
<div>{{ bag.created_date }}</div>
<div class="hidden md:block font-normal text-text-muted">{{ bag.created_time }}</div>
@ -44,20 +46,24 @@
<td data-label="Roaster" class="px-4 py-3 whitespace-nowrap font-medium text-text-secondary">
{{ bag.roaster_name }}
</td>
<td data-label="Roast" class="card-title px-4 py-3 whitespace-nowrap">{{ bag.roast_name }}</td>
<td data-label="Weight" class="px-4 py-3 whitespace-nowrap">{{ bag.amount }}g</td>
<td data-label="Status" class="px-4 py-3 whitespace-nowrap">
<td data-label="Roast" class="card-title px-4 py-3 whitespace-nowrap">
{{ bag.roast_name }}
</td>
<td data-label="Weight" class="px-4 py-3 whitespace-nowrap text-xs font-medium text-text-secondary">{{ bag.amount }}g</td>
<td data-label="Status" class="mobile-hidden px-4 py-3 whitespace-nowrap md:text-center">
{% if bag.closed %}
Closed
<span class="pill pill-warning">Closed</span>
{% else %}
<span class="font-medium text-emerald-600">Open</span>
<div class="hidden md:block text-xs text-text-muted">{{ bag.remaining }}g left</div>
<span class="pill pill-success">Open</span>
<div class="text-xs text-text-muted">{{ bag.remaining }}g left</div>
{% endif %}
</td>
{% if !bag.closed %}
<td data-label="Remaining" class="px-4 py-3 whitespace-nowrap md:hidden">{{ bag.remaining }}g left</td>
{% endif %}
<td data-label="Finished" class="px-4 py-3 whitespace-nowrap">{{ bag.finished_at }}</td>
<td data-label="Finished" class="whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
<div>{{ bag.finished_date }}</div>
</td>
{% if is_authenticated %}
<td data-label="" class="card-detail hidden">
<div class="flex items-center gap-4">
@ -76,13 +82,21 @@
</div>
</td>
<td data-label="" class="card-actions px-4 py-3 text-right">
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
</button>
<div class="inline-flex items-center gap-2">
<span class="md:hidden">
{% if bag.closed %}
<span class="pill pill-warning text-[0.625rem] py-0 px-1.5">Closed</span>
{% else %}
<span class="pill pill-success text-[0.625rem] py-0 px-1.5">Open</span>
{% endif %}
</span>
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
</button>
</div>
</td>
{% endif %}
</tr>

View file

@ -58,13 +58,15 @@
<th scope="col" class="px-4 py-3">Brewer</th>
<th scope="col" class="px-4 py-3">Notes</th>
{% if is_authenticated %}
<th scope="col" class="px-4 py-3 text-right">Actions</th>
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
{% endif %}
</tr>
</thead>
<tbody class="divide-y/70">
{% for brew in brews.items %}
<tr class="transition hover:bg-surface-alt">
<tr class="transition hover:bg-surface-alt"
{% if is_authenticated %}onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"{% endif %}
>
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
<div>{{ brew.created_date }}</div>
<div class="hidden md:block font-normal text-text-muted">{{ brew.created_time }}</div>
@ -80,7 +82,7 @@
</td>
<td data-label="Grinder" class="px-4 py-3 whitespace-nowrap md:hidden">{{ brew.grinder_name }}</td>
<td data-label="Recipe" class="px-4 py-3 whitespace-nowrap">
<div>{{ brew.coffee_weight }} / {{ brew.water_volume }}</div>
<div>{{ brew.coffee_weight }} · {{ brew.water_volume }}</div>
<div class="hidden md:block text-xs text-text-muted">{{ brew.ratio }} · {{ brew.water_temp }}</div>
</td>
<td data-label="Ratio" class="px-4 py-3 whitespace-nowrap md:hidden">{{ brew.ratio }}</td>
@ -94,11 +96,15 @@
{% if let Some(fp_name) = brew.filter_paper_name %}
<td data-label="Filter" class="px-4 py-3 whitespace-nowrap md:hidden">{{ fp_name }}</td>
{% endif %}
<td data-label="Notes" class="px-4 py-3 text-xs text-text-muted">
{% if !brew.quick_notes_label.is_empty() %}
{{ brew.quick_notes_label }}
<td data-label="Notes" class="px-4 py-3">
{% if !brew.quick_notes.is_empty() %}
<div class="flex flex-wrap gap-1">
{% for qn in brew.quick_notes %}
<span class="{{ qn.pill_class }}">{{ qn.label }}</span>
{% endfor %}
</div>
{% else %}
<span class="text-text-muted">&mdash;</span>
<span class="pill pill-muted">No Notes</span>
{% endif %}
</td>
{% if is_authenticated %}
@ -108,7 +114,6 @@
<td data-label="" class="card-actions px-4 py-3 text-right">
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>

View file

@ -25,8 +25,9 @@
<tr>
{% call table::sortable_header("Added", "created-at", navigator, "#cafe-list") %}
{% call table::sortable_header("Name", "name", navigator, "#cafe-list") %}
{% call table::sortable_header("Location", "country", navigator, "#cafe-list") %}
<th scope="col" class="px-4 py-3 text-right">Actions</th>
{% call table::sortable_header("City", "city", navigator, "#cafe-list") %}
{% call table::sortable_header("Country", "country", navigator, "#cafe-list") %}
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y/70">
@ -38,6 +39,7 @@
data-sort-country="{{ cafe.country }}"
data-sort-city="{{ cafe.city }}"
class="transition hover:bg-surface-alt"
onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"
>
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
<div>{{ cafe.created_date }}</div>
@ -46,11 +48,10 @@
<td data-label="Name" class="card-title px-4 py-3 font-medium text-text">
{{ cafe.name }}
</td>
<td data-label="Location" class="px-4 py-3 whitespace-nowrap">
{{ cafe.country }}
<div class="hidden md:block text-xs text-text-muted">{{ cafe.city }}</div>
</td>
<td data-label="Location" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cafe.country }}</td>
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cafe.city }}</td>
<td data-label="City" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ cafe.city }}</td>
<td data-label="Country" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ cafe.country }}</td>
<td data-label="" class="card-detail hidden">
<div class="flex items-center gap-4">
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
@ -75,7 +76,6 @@
<td data-label="" class="card-actions px-4 py-3 text-right">
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
@ -83,7 +83,7 @@
</td>
</tr>
<tr class="hidden detail-row">
<td colspan="4" class="bg-surface-alt px-4 py-2">
<td colspan="5" class="bg-surface-alt px-4 py-2">
<div class="flex items-center gap-4">
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
href="{{ cafe.map_url }}" target="_blank" rel="noreferrer noopener">

View file

@ -24,10 +24,12 @@
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
<tr>
{% call table::sortable_header("Added", "created-at", navigator, "#cup-list") %}
{% call table::sortable_header("Coffee", "roast", navigator, "#cup-list") %}
{% call table::sortable_header("Roast", "roast", navigator, "#cup-list") %}
{% call table::sortable_header("Roaster", "roaster", navigator, "#cup-list") %}
{% call table::sortable_header("Cafe", "cafe", navigator, "#cup-list") %}
{% call table::sortable_header("City", "city", navigator, "#cup-list") %}
{% if is_authenticated %}
<th scope="col" class="px-4 py-3 text-right">Actions</th>
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
{% endif %}
</tr>
</thead>
@ -36,19 +38,23 @@
<tr
data-star-key="{{ cup.id }}"
class="transition hover:bg-surface-alt"
{% if is_authenticated %}onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"{% endif %}
>
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
<div>{{ cup.created_date }}</div>
<div class="hidden md:block font-normal text-text-muted">{{ cup.created_time }}</div>
</td>
<td data-label="Coffee" class="card-title px-4 py-3 whitespace-nowrap">
<td data-label="Coffee" class="card-title px-4 py-3 whitespace-nowrap md:hidden">
<div class="font-medium text-text">{{ cup.roast_name }}</div>
<div class="hidden md:block text-xs text-text-muted">{{ cup.roaster_name }}</div>
</td>
<td data-label="Roaster" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cup.roaster_name }}</td>
<td data-label="Roast" class="mobile-hidden px-4 py-3 whitespace-nowrap font-medium text-text">{{ cup.roast_name }}</td>
<td data-label="Roaster" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ cup.roaster_name }}</td>
<td data-label="Cafe" class="px-4 py-3 whitespace-nowrap">
{{ cup.cafe_name }}
</td>
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cup.cafe_city }}</td>
<td data-label="City" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ cup.cafe_city }}</td>
{% if is_authenticated %}
<td data-label="" class="card-detail hidden">
<div class="flex items-center gap-4">
@ -62,7 +68,6 @@
<td data-label="" class="card-actions px-4 py-3 text-right">
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
@ -72,7 +77,7 @@
</tr>
{% if is_authenticated %}
<tr class="hidden detail-row">
<td colspan="4" class="bg-surface-alt px-4 py-2">
<td colspan="6" class="bg-surface-alt px-4 py-2">
<div class="flex items-center gap-4">
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"

View file

@ -29,13 +29,15 @@
{% call table::sortable_header("Make", "make", navigator, "#gear-list") %}
{% call table::sortable_header("Model", "model", navigator, "#gear-list") %}
{% if is_authenticated %}
<th scope="col" class="px-4 py-3 text-right">Actions</th>
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
{% endif %}
</tr>
</thead>
<tbody class="divide-y/70">
{% for item in gear.items %}
<tr class="transition hover:bg-surface-alt">
<tr class="transition hover:bg-surface-alt"
{% if is_authenticated %}onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"{% endif %}
>
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
<div>{{ item.created_date }}</div>
<div class="hidden md:block font-normal text-text-muted">{{ item.created_time }}</div>
@ -63,7 +65,6 @@
<td data-label="" class="card-actions px-4 py-3 text-right">
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>

View file

@ -28,7 +28,7 @@
{% call table::sortable_header("Origin", "origin", navigator, "#roast-list") %}
<th scope="col" class="px-4 py-3">Tasting Notes</th>
{% if is_authenticated %}
<th scope="col" class="px-4 py-3 text-right">Actions</th>
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
{% endif %}
</tr>
</thead>
@ -42,6 +42,7 @@
data-sort-origin="{{ roast.origin }}"
data-sort-producer="{{ roast.producer }}"
class="transition hover:bg-surface-alt"
{% if is_authenticated %}onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"{% endif %}
>
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
<div>{{ roast.created_date }}</div>
@ -61,11 +62,15 @@
{% if !roast.producer.is_empty() %}
<td data-label="Producer" class="px-4 py-3 whitespace-nowrap md:hidden">{{ roast.producer }}</td>
{% endif %}
<td data-label="Tasting Notes" class="px-4 py-3 text-xs text-text-muted text-right md:text-left">
{% if roast.tasting_notes.is_empty() %}
<span class="text-text-muted">&mdash;</span>
<td data-label="Tasting Notes" class="px-4 py-3">
{% if !roast.tasting_notes.is_empty() %}
<div class="flex flex-wrap gap-1 md:justify-start justify-end">
{% for note in roast.tasting_notes %}
<span class="pill pill-muted">{{ note }}</span>
{% endfor %}
</div>
{% else %}
{{ roast.tasting_notes.join(", ") }}
<span class="pill pill-muted">No Notes</span>
{% endif %}
</td>
{% if is_authenticated %}
@ -81,7 +86,6 @@
<td data-label="" class="card-actions px-4 py-3 text-right">
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>

View file

@ -25,8 +25,9 @@
<tr>
{% call table::sortable_header("Added", "created-at", navigator, "#roaster-list") %}
{% call table::sortable_header("Name", "name", navigator, "#roaster-list") %}
{% call table::sortable_header("Location", "country", navigator, "#roaster-list") %}
<th scope="col" class="px-4 py-3 text-right">Actions</th>
{% call table::sortable_header("Country", "country", navigator, "#roaster-list") %}
{% call table::sortable_header("City", "city", navigator, "#roaster-list") %}
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y/70">
@ -38,6 +39,7 @@
data-sort-country="{{ roaster.country }}"
data-sort-city="{{ roaster.city }}"
class="transition hover:bg-surface-alt"
{% if roaster.has_homepage || is_authenticated %}onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"{% endif %}
>
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
<div>{{ roaster.created_date }}</div>
@ -46,15 +48,12 @@
<td data-label="Name" class="card-title px-4 py-3 font-medium text-text">
{{ roaster.name }}
</td>
<td data-label="Location" class="px-4 py-3 whitespace-nowrap">
{{ roaster.country }}
{% if !roaster.city.is_empty() %}
<div class="hidden md:block text-xs text-text-muted">{{ roaster.city }}</div>
{% endif %}
</td>
<td data-label="Location" class="px-4 py-3 whitespace-nowrap md:hidden">{{ roaster.country }}</td>
{% if !roaster.city.is_empty() %}
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ roaster.city }}</td>
{% endif %}
<td data-label="Country" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ roaster.country }}</td>
<td data-label="City" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ roaster.city }}</td>
{% if roaster.has_homepage || is_authenticated %}
<td data-label="" class="card-detail hidden">
<div class="flex items-center gap-4">
@ -78,7 +77,6 @@
{% if roaster.has_homepage || is_authenticated %}
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
@ -88,7 +86,7 @@
</tr>
{% if roaster.has_homepage || is_authenticated %}
<tr class="hidden detail-row">
<td colspan="4" class="bg-surface-alt px-4 py-2">
<td colspan="5" class="bg-surface-alt px-4 py-2">
<div class="flex items-center gap-4">
{% if roaster.has_homepage %}
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"