fix(web): better alignment on mobile for table views

This commit is contained in:
Jon Seager 2026-02-03 10:54:40 +00:00
parent 32c0935124
commit ff8f0c781e
No known key found for this signature in database
6 changed files with 89 additions and 36 deletions

View file

@ -97,7 +97,7 @@
{% call table::pagination_header(bags, navigator, "#bag-list") %} {% call table::pagination_header(bags, navigator, "#bag-list") %}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-amber-200 text-left text-sm text-stone-700"> <table class="responsive-table min-w-full divide-y divide-amber-200 text-left text-sm text-stone-700">
<thead class="bg-amber-200/60 text-xs font-semibold tracking-wide text-amber-900"> <thead class="bg-amber-200/60 text-xs font-semibold tracking-wide text-amber-900">
<tr> <tr>
{% call table::sortable_header("Roaster", "roaster", navigator, "#bag-list") %} {% {% call table::sortable_header("Roaster", "roaster", navigator, "#bag-list") %} {%
@ -110,13 +110,13 @@
<tbody class="divide-y divide-amber-200/70"> <tbody class="divide-y divide-amber-200/70">
{% for bag in bags.items %} {% for bag in bags.items %}
<tr class="bg-amber-50/40 transition hover:bg-amber-50"> <tr class="bg-amber-50/40 transition hover:bg-amber-50">
<td class="px-4 py-3 whitespace-nowrap font-medium text-stone-600"> <td data-label="Roaster" class="px-4 py-3 whitespace-nowrap font-medium text-stone-600">
{{ bag.roaster_name }} {{ bag.roaster_name }}
</td> </td>
<td class="px-4 py-3 whitespace-nowrap">{{ bag.roast_name }}</td> <td data-label="Roast" class="px-4 py-3 whitespace-nowrap">{{ bag.roast_name }}</td>
<td class="px-4 py-3 whitespace-nowrap">{{ bag.amount }}g</td> <td data-label="Weight" class="px-4 py-3 whitespace-nowrap">{{ bag.amount }}g</td>
<td class="px-4 py-3 whitespace-nowrap">{{ bag.created_at }}</td> <td data-label="Added" class="px-4 py-3 whitespace-nowrap">{{ bag.created_at }}</td>
<td class="px-4 py-3 whitespace-nowrap">{{ bag.finished_at }}</td> <td data-label="Finished" class="px-4 py-3 whitespace-nowrap">{{ bag.finished_at }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View file

@ -5,7 +5,7 @@
{% call table::pagination_header(brews, navigator, "#brew-list") %} {% call table::pagination_header(brews, navigator, "#brew-list") %}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-amber-200 text-left text-sm text-stone-700"> <table class="responsive-table min-w-full divide-y divide-amber-200 text-left text-sm text-stone-700">
<thead class="bg-amber-200/60 text-xs font-semibold tracking-wide text-amber-900"> <thead class="bg-amber-200/60 text-xs font-semibold tracking-wide text-amber-900">
<tr> <tr>
<th scope="col" class="px-4 py-3">Coffee</th> <th scope="col" class="px-4 py-3">Coffee</th>
@ -22,7 +22,7 @@
<tbody class="divide-y divide-amber-200/70"> <tbody class="divide-y divide-amber-200/70">
{% for brew in brews.items %} {% for brew in brews.items %}
<tr class="bg-amber-50/40 transition hover:bg-amber-50"> <tr class="bg-amber-50/40 transition hover:bg-amber-50">
<td class="px-4 py-3 whitespace-nowrap"> <td data-label="Coffee" class="px-4 py-3 whitespace-nowrap">
<div class="font-medium text-stone-800"> <div class="font-medium text-stone-800">
<a href="/roasters/{{ brew.roaster_slug }}/roasts/{{ brew.roast_slug }}" class="hover:text-amber-700 hover:underline"> <a href="/roasters/{{ brew.roaster_slug }}/roasts/{{ brew.roast_slug }}" class="hover:text-amber-700 hover:underline">
{{ brew.roast_name }} {{ brew.roast_name }}
@ -34,24 +34,24 @@
</a> </a>
</div> </div>
</td> </td>
<td class="px-4 py-3 whitespace-nowrap"> <td data-label="Grind" class="px-4 py-3 whitespace-nowrap">
<div>{{ brew.grind_setting }}</div> <div>{{ brew.grind_setting }}</div>
<div class="text-xs text-stone-500">{{ brew.grinder_name }}</div> <div class="text-xs text-stone-500">{{ brew.grinder_name }}</div>
</td> </td>
<td class="px-4 py-3 whitespace-nowrap"> <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="text-xs text-stone-500 font-mono">{{ brew.ratio }}</div> <div class="text-xs text-stone-500 font-mono">{{ brew.ratio }}</div>
</td> </td>
<td class="px-4 py-3 whitespace-nowrap">{{ brew.water_temp }}</td> <td data-label="Temp" class="px-4 py-3 whitespace-nowrap">{{ brew.water_temp }}</td>
<td class="px-4 py-3 whitespace-nowrap"> <td data-label="Brewer" class="px-4 py-3 whitespace-nowrap">
<div>{{ brew.brewer_name }}</div> <div>{{ brew.brewer_name }}</div>
{% if let Some(fp_name) = brew.filter_paper_name %} {% if let Some(fp_name) = brew.filter_paper_name %}
<div class="text-xs text-stone-500">{{ fp_name }}</div> <div class="text-xs text-stone-500">{{ fp_name }}</div>
{% endif %} {% endif %}
</td> </td>
<td class="px-4 py-3 whitespace-nowrap text-stone-500">{{ brew.created_at }}</td> <td data-label="Date" class="px-4 py-3 whitespace-nowrap text-stone-500">{{ brew.created_at }}</td>
{% if is_authenticated %} {% if is_authenticated %}
<td class="px-4 py-3 text-right"> <td data-label="" class="px-4 py-3 text-right">
<form class="inline" data-on:submit="@post('/api/v1/brews?{{ navigator.query() }}', {contentType: 'form', responseOverrides: {selector: '#brew-list', mode: 'replace'}})"> <form class="inline" data-on:submit="@post('/api/v1/brews?{{ navigator.query() }}', {contentType: 'form', responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
<input type="hidden" name="bag_id" value="{{ brew.bag_id }}" /> <input type="hidden" name="bag_id" value="{{ brew.bag_id }}" />
<input type="hidden" name="coffee_weight" value="{{ brew.coffee_weight_raw }}" /> <input type="hidden" name="coffee_weight" value="{{ brew.coffee_weight_raw }}" />

View file

@ -13,7 +13,7 @@
{% call table::pagination_header(gear, navigator, "#gear-list") %} {% call table::pagination_header(gear, navigator, "#gear-list") %}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-amber-200 text-left text-sm text-stone-700"> <table class="responsive-table min-w-full divide-y divide-amber-200 text-left text-sm text-stone-700">
<thead class="bg-amber-200/60 text-xs font-semibold tracking-wide text-amber-900"> <thead class="bg-amber-200/60 text-xs font-semibold tracking-wide text-amber-900">
<tr> <tr>
{% call table::sortable_header("Category", "category", navigator, "#gear-list") %} {% call table::sortable_header("Category", "category", navigator, "#gear-list") %}
@ -28,22 +28,22 @@
<tbody class="divide-y divide-amber-200/70"> <tbody class="divide-y divide-amber-200/70">
{% for item in gear.items %} {% for item in gear.items %}
<tr class="bg-amber-50/40 transition hover:bg-amber-50"> <tr class="bg-amber-50/40 transition hover:bg-amber-50">
<td class="px-4 py-3 whitespace-nowrap"> <td data-label="Category" class="px-4 py-3 whitespace-nowrap">
<span class="inline-flex rounded-full bg-amber-100 px-2 py-1 text-xs font-medium text-amber-800"> <span class="inline-flex rounded-full bg-amber-100 px-2 py-1 text-xs font-medium text-amber-800">
{{ item.category_label }} {{ item.category_label }}
</span> </span>
</td> </td>
<td class="px-4 py-3 whitespace-nowrap font-medium text-stone-600"> <td data-label="Make" class="px-4 py-3 whitespace-nowrap font-medium text-stone-600">
{{ item.make }} {{ item.make }}
</td> </td>
<td class="px-4 py-3 whitespace-nowrap"> <td data-label="Model" class="px-4 py-3 whitespace-nowrap">
{{ item.model }} {{ item.model }}
</td> </td>
<td class="px-4 py-3 whitespace-nowrap"> <td data-label="Added" class="px-4 py-3 whitespace-nowrap">
{{ item.created_at }} {{ item.created_at }}
</td> </td>
{% if is_authenticated %} {% if is_authenticated %}
<td class="px-4 py-3 text-right"> <td data-label="" class="px-4 py-3 text-right">
<button <button
type="button" type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500" class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"

View file

@ -11,7 +11,7 @@
<section class="rounded-lg border border-amber-300 bg-amber-100/80 shadow-sm"> <section class="rounded-lg border border-amber-300 bg-amber-100/80 shadow-sm">
{% call table::pagination_header(roasts, navigator, "#roast-list") %} {% call table::pagination_header(roasts, navigator, "#roast-list") %}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-amber-200 text-left text-sm text-stone-700"> <table class="responsive-table min-w-full divide-y divide-amber-200 text-left text-sm text-stone-700">
<thead class="bg-amber-200/60 text-xs font-semibold tracking-wide text-amber-900"> <thead class="bg-amber-200/60 text-xs font-semibold tracking-wide text-amber-900">
<tr> <tr>
{% call table::sortable_header("Added", "created-at", navigator, "#roast-list") %} {% {% call table::sortable_header("Added", "created-at", navigator, "#roast-list") %} {%
@ -36,10 +36,10 @@
data-sort-producer="{{ roast.producer }}" data-sort-producer="{{ roast.producer }}"
class="bg-amber-50/40 transition hover:bg-amber-50" class="bg-amber-50/40 transition hover:bg-amber-50"
> >
<td class="whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600"> <td data-label="Added" class="whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
{{ roast.created_at }} {{ roast.created_at }}
</td> </td>
<td class="px-4 py-3"> <td data-label="Roast" class="px-4 py-3">
<div class="flex flex-col"> <div class="flex flex-col">
<a <a
href="{{ roast.detail_path }}" href="{{ roast.detail_path }}"
@ -48,10 +48,10 @@
> >
</div> </div>
</td> </td>
<td class="px-4 py-3 whitespace-nowrap">{{ roast.roaster_label }}</td> <td data-label="Roaster" class="px-4 py-3 whitespace-nowrap">{{ roast.roaster_label }}</td>
<td class="px-4 py-3 whitespace-nowrap">{{ roast.origin }}</td> <td data-label="Origin" class="px-4 py-3 whitespace-nowrap">{{ roast.origin }}</td>
<td class="px-4 py-3 whitespace-nowrap">{{ roast.producer }}</td> <td data-label="Producer" class="px-4 py-3 whitespace-nowrap">{{ roast.producer }}</td>
<td class="px-4 py-3"> <td data-label="Notes" class="px-4 py-3">
{% if roast.tasting_notes.is_empty() %} {% if roast.tasting_notes.is_empty() %}
<span class="text-xs text-stone-500">No tasting notes yet.</span> <span class="text-xs text-stone-500">No tasting notes yet.</span>
{% else %} {% else %}
@ -68,7 +68,7 @@
{% endif %} {% endif %}
</td> </td>
{% if is_authenticated %} {% if is_authenticated %}
<td class="px-4 py-3 text-right"> <td data-label="" class="px-4 py-3 text-right">
<button <button
type="button" type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500" class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"

View file

@ -13,7 +13,7 @@
<section class="rounded-lg border border-amber-300 bg-amber-100/80 shadow-sm"> <section class="rounded-lg border border-amber-300 bg-amber-100/80 shadow-sm">
{% call table::pagination_header(roasters, navigator, "#roaster-list") %} {% call table::pagination_header(roasters, navigator, "#roaster-list") %}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-amber-200 text-left text-sm text-stone-700"> <table class="responsive-table min-w-full divide-y divide-amber-200 text-left text-sm text-stone-700">
<thead class="bg-amber-200/60 text-xs font-semibold tracking-wide text-amber-900"> <thead class="bg-amber-200/60 text-xs font-semibold tracking-wide text-amber-900">
<tr> <tr>
{% call table::sortable_header("Added", "created-at", navigator, "#roaster-list") %} {% {% call table::sortable_header("Added", "created-at", navigator, "#roaster-list") %} {%
@ -37,19 +37,19 @@
data-sort-city="{{ roaster.city }}" data-sort-city="{{ roaster.city }}"
class="bg-amber-50/40 transition hover:bg-amber-50" class="bg-amber-50/40 transition hover:bg-amber-50"
> >
<td class="whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600"> <td data-label="Added" class="whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
{{ roaster.created_at }} {{ roaster.created_at }}
</td> </td>
<td class="px-4 py-3"> <td data-label="Name" class="px-4 py-3">
<a <a
href="{{ roaster.detail_path }}" href="{{ roaster.detail_path }}"
class="font-semibold text-amber-800 hover:text-amber-600" class="font-semibold text-amber-800 hover:text-amber-600"
>{{ roaster.name }}</a >{{ roaster.name }}</a
> >
</td> </td>
<td class="px-4 py-3 whitespace-nowrap">{{ roaster.country }}</td> <td data-label="Country" class="px-4 py-3 whitespace-nowrap">{{ roaster.country }}</td>
<td class="px-4 py-3 whitespace-nowrap">{{ roaster.city }}</td> <td data-label="City" class="px-4 py-3 whitespace-nowrap">{{ roaster.city }}</td>
<td class="px-4 py-3"> <td data-label="Homepage" class="mobile-hidden px-4 py-3">
{% if roaster.has_homepage %} {% if roaster.has_homepage %}
<a <a
class="inline-flex items-center justify-center rounded-md border border-amber-400 bg-amber-200/50 p-2 text-amber-800 transition hover:border-amber-500 hover:text-amber-900" class="inline-flex items-center justify-center rounded-md border border-amber-400 bg-amber-200/50 p-2 text-amber-800 transition hover:border-amber-500 hover:text-amber-900"
@ -73,9 +73,9 @@
<span class="text-stone-400">&mdash;</span> <span class="text-stone-400">&mdash;</span>
{% endif %} {% endif %}
</td> </td>
<td class="px-4 py-3 text-sm text-stone-600">{{ roaster.notes }}</td> <td data-label="Notes" class="mobile-hidden px-4 py-3 text-sm text-stone-600">{{ roaster.notes }}</td>
{% if is_authenticated %} {% if is_authenticated %}
<td class="px-4 py-3 text-right"> <td data-label="" class="px-4 py-3 text-right">
<button <button
type="button" type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500" class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"

View file

@ -96,6 +96,59 @@ a {
display: block; display: block;
} }
/* Responsive tables: convert rows to cards on mobile */
@media (max-width: 767px) {
.responsive-table thead {
display: none;
}
.responsive-table tbody {
display: block;
}
.responsive-table tr {
display: block;
padding: 1rem;
border-bottom: 1px solid rgba(217, 169, 55, 0.35);
}
.responsive-table tr:last-child {
border-bottom: none;
}
.responsive-table td {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 0.25rem 0;
white-space: normal;
font-size: 0.875rem; /* normalize all values to text-sm */
}
.responsive-table td::before {
content: attr(data-label);
flex-shrink: 0;
min-width: 5.5rem;
font-size: 0.8125rem;
font-weight: 600;
color: #78350f; /* amber-900 */
margin-right: 0.75rem;
}
.responsive-table td[data-label=""]::before {
display: none;
}
.responsive-table td[data-label=""] {
justify-content: flex-end;
padding-top: 0.25rem;
}
.responsive-table td.mobile-hidden {
display: none;
}
}
/* Desktop: alternating cards with central line */ /* Desktop: alternating cards with central line */
@media (min-width: 768px) { @media (min-width: 768px) {
.timeline-heading { .timeline-heading {