diff --git a/src/flask/static/css/app.css b/src/flask/static/css/app.css
index fb6acd1..b419c99 100644
--- a/src/flask/static/css/app.css
+++ b/src/flask/static/css/app.css
@@ -451,7 +451,10 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }
/* ---------------------------------------------------------------- tables */
-.table-wrap { overflow-x: auto; }
+.table-wrap {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
table.data {
width: 100%;
@@ -480,21 +483,18 @@ table.data tbody tr:hover { background: var(--bg-raised-2); }
table.data .num { font-family: var(--mono); text-align: right; white-space: nowrap; }
table.data .comment { color: var(--text-muted); font-size: .84rem; }
-/* On a narrow screen the tables reflow into stacked cards.
+/* A heading that is too long for a phone column. The long form shows
+ on a desktop and the short form on a phone - see the media query. */
+table.data .short { display: none; }
+table.data .advice-trigger .short { display: none; }
- A seven-column table does not fit a phone. It was left to scroll
- sideways inside its wrapper, which meant the columns on the right were
- off the screen and the brewer had to pan the table to see them.
+/* On a narrow screen the header wraps onto two rows: the brand and the
+ sign-out on the first, the nav tabs on the second where they get the
+ full width.
- Each row becomes a card: the header cell becomes a label above its
- value, and the cells stack. */
+ They used to sit on one row with the brand and the sign-out, and the
+ last tab was cut off mid-word. */
@media (max-width: 720px) {
- /* The header wraps onto two rows on a phone: the brand and the
- sign-out on the first, the nav tabs on the second where they get
- the full width and scroll sideways if they still do not fit.
-
- They used to sit on one row with the brand and the sign-out, and
- the last tab was cut off mid-word. */
.topbar__inner {
flex-wrap: wrap;
row-gap: .5rem;
@@ -537,79 +537,97 @@ table.data .comment { color: var(--text-muted); font-size: .84rem; }
letter-spacing: .01em;
}
+ /* The tables stay tables on a phone.
+
+ They scroll sideways inside their card, which is what a table
+ does. They used to be squeezed to fit instead, and with seven
+ to nine columns on a 390px screen that meant every heading and
+ every value was truncated to a few characters - a gravity of
+ "1.0..." and a date of "202..." are worse than useless.
+
+ So the columns keep their natural width, the table scrolls, and
+ the advice is a small button so the column that matters is
+ narrow. */
+ table.data {
+ table-layout: auto;
+ width: 100%;
+ }
+
+ table.data th,
+ table.data td {
+ padding-left: .5rem;
+ padding-right: .5rem;
+ }
+
+ /* A name and a comment are words, so they get a sensible minimum
+ width and wrap on a word boundary - not one word per line. */
+ table.data td.col-name,
+ table.data td.col-comment {
+ min-width: 7rem;
+ max-width: 12rem;
+ white-space: normal;
+ word-break: normal;
+ overflow-wrap: break-word;
+ }
+
+ /* The heading gets a short form, because a heading that is cut off
+ is worse than a heading that is brief. */
+ table.data .col-abv .long,
+ table.data .col-advice .long,
+ table.data .col-finished .long,
+ table.data .col-comment .long {
+ display: none;
+ }
+
+ table.data .col-abv .short,
+ table.data .col-advice .short,
+ table.data .col-finished .short,
+ table.data .col-comment .short {
+ display: inline;
+ }
+
+ /* The advice trigger is small on a phone - it is a button, and
+ the advice itself is in the pop-out. */
+ table.data .advice-trigger {
+ font-size: .68rem;
+ padding: .25rem .4rem;
+ gap: .2rem;
+ }
+
+ table.data .advice-trigger .long {
+ display: none;
+ }
+
+ table.data .advice-trigger .short {
+ display: inline;
+ }
+
+ /* The action column holds a button. */
+ table.data .col-action {
+ white-space: nowrap;
+ }
+
+ table.data .col-action .btn {
+ padding-left: .5rem;
+ padding-right: .5rem;
+ font-size: .72rem;
+ }
+
+ /* The secondary columns go away on a phone - the details are still
+ on the run's own page. */
+ table.data .col-drop,
+ table.data .col-legacy,
+ table.data .col-days {
+ display: none;
+ }
+
+ /* The scroll area keeps a visible scrollbar on a phone, so it is
+ obvious there is more of the table to the right. */
.table-wrap {
- overflow-x: visible;
+ scrollbar-width: thin;
+ -webkit-overflow-scrolling: touch;
}
- table.data,
- table.data tbody,
- table.data tr,
- table.data td {
- display: block;
- width: auto;
- }
-
- table.data thead {
- /* HIDDEN in the stacked layout. Each cell shows its column
- name as a label, so the header row would be a duplicate - and
- it was 230px of the first card. */
- display: none;
- }
-
- table.data tr {
- border: 1px solid var(--border);
- border-radius: 8px;
- margin-bottom: .75rem;
- padding: .25rem .1rem;
- background: var(--bg-raised-2);
- }
-
- table.data tbody tr:hover {
- background: var(--bg-raised-2);
- }
-
- table.data th {
- display: block;
- border-bottom: 1px solid var(--border);
- }
-
- table.data td {
- display: grid;
- grid-template-columns: 8.5rem 1fr;
- gap: .5rem;
- align-items: baseline;
- padding: .45rem .7rem;
- border-bottom: 1px solid rgba(255, 255, 255, .04);
- text-align: left;
- }
-
- table.data td::before {
- /* The column heading, shown as the row's label. */
- content: attr(data-label);
- font-size: .65rem;
- font-weight: 650;
- letter-spacing: .07em;
- text-transform: uppercase;
- color: var(--text-faint);
- }
-
- table.data td.num {
- text-align: left;
- }
-
- table.data td.right,
- table.data td:empty {
- grid-template-columns: 1fr;
- }
-
- table.data td.right::before {
- content: none;
- }
-
- /* A cell with nothing in it must not leave a gap. */
- table.data td:empty {
- display: none;
- }
}
/* ----------------------------------------------------------------- chips */
diff --git a/src/flask/static/js/app.js b/src/flask/static/js/app.js
index dd131fc..a8b0400 100644
--- a/src/flask/static/js/app.js
+++ b/src/flask/static/js/app.js
@@ -207,36 +207,12 @@
}
}
- function labelTableCells() {
- var tables = document.querySelectorAll("table.data");
- for (var t = 0; t < tables.length; t++) {
- var headerRow = tables[t].querySelector("thead tr");
- if (!headerRow) { continue; }
- var labels = [];
- var headers = headerRow.querySelectorAll("th");
- for (var h = 0; h < headers.length; h++) {
- labels.push(headers[h].textContent.trim());
- }
-
- var rows = tables[t].querySelectorAll("tbody tr");
- for (var r = 0; r < rows.length; r++) {
- var cells = rows[r].querySelectorAll("td");
- for (var c = 0; c < cells.length && c < labels.length; c++) {
- if (labels[c] && !cells[c].getAttribute("data-label")) {
- cells[c].setAttribute("data-label", labels[c]);
- }
- }
- }
- }
- }
-
function init() {
focusFirstEmpty();
defaultDateFields();
wireToggles();
wireConfirms();
wireAdvicePopouts();
- labelTableCells();
}
if (document.readyState === "loading") {
diff --git a/src/flask/templates/analysis.html b/src/flask/templates/analysis.html
index 7476bdd..57f1704 100644
--- a/src/flask/templates/analysis.html
+++ b/src/flask/templates/analysis.html
@@ -99,43 +99,43 @@
-
Run
-
Name
-
OG
-
FG
-
ABV
-
Attenuation
-
Days
-
To 1.010
-
Finished
+
Run
+
Name
+
OG
+
FG
+
ABV
+
AttenuationAtt.
+
Days
+
To 1.0101.010
+
FinishedDone
{% for row in rows %}
-
{{ row.beer_run_id }}
-
{{ row.name }}{% if row.suspect %} not counted{% endif %}
-
+
{{ row.beer_run_id }}
+
{{ row.name }}{% if row.suspect %} not counted{% endif %}
+
{% if row.og_points is not none %}
{{ '%.3f' % (row.og_points / 1000) }}
{% else %}—{% endif %}
-
+
{% if row.fg_points is not none %}
{{ '%.3f' % (row.fg_points / 1000) }}
{% else %}—{% endif %}
-
+
{% if row.abv is not none %}{{ '%.2f' % row.abv }}%{% else %}—{% endif %}
-
+
{% if row.attenuation is not none %}
{{ row.attenuation }}%
{% else %}—{% endif %}
-
{{ row.days if row.days is not none else '—' }}
-
{{ row.days_to_1010 if row.days_to_1010 is not none else '—' }}
-
{{ row.final_date or '—' }}
+
{{ row.days if row.days is not none else '—' }}
+
{{ row.days_to_1010 if row.days_to_1010 is not none else '—' }}
{% endfor %}
diff --git a/tests/test_brew_advice.py b/tests/test_brew_advice.py
index 8fc5e98..a9c438b 100644
--- a/tests/test_brew_advice.py
+++ b/tests/test_brew_advice.py
@@ -1024,6 +1024,31 @@ def test_the_advice_in_the_table_is_a_popout_not_the_text(client, repo):
assert "Warm it to 21C and wait." in body
+def test_the_tables_keep_their_columns_on_a_phone(client, repo):
+ """The tables are readable on a phone, not squeezed into nothing.
+
+ The advice is a button and the table scrolls, so nothing is
+ truncated to an unreadable stub.
+ """
+ repo.users.insert_one({"username": "beer", "password": "beer_pass"})
+ client.post("/login", data={"username": "beer", "password": "beer_pass"},
+ follow_redirects=True)
+
+ seed_readings(repo, [("1", "Cider raspberry elderberry", "1045", "2026-01-01", False, "")])
+ doc = repo.readings.find_one({"beer_run_id": "1"})
+ repo.set_reading_advice(str(doc["_id"]), "Warm it to 21C and wait.", ok=True)
+
+ body = client.get("/brews").get_data(as_text=True)
+
+ # The long heading has a short form for a phone, and the long form
+ # is still in the page for a desktop.
+ assert "Brewing advice" in body
+ assert ">Advice<" in body.replace("\n", "")
+
+ # The beer name is not truncated in the markup.
+ assert "Cider raspberry elderberry" in body
+
+
def test_a_failed_advice_is_marked_as_such(client, repo):
"""A generation failure reads differently from real advice."""
repo.users.insert_one({"username": "beer", "password": "beer_pass"})