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 @@ - - - - - - - - - + + + + + + + + + {% for row in rows %} - - - + + - - - - - - + + + {% endfor %} @@ -155,21 +155,21 @@
RunNameOGFGABVAttenuationDaysTo 1.010FinishedRunNameOGFGABVAttenuationAtt.DaysTo 1.0101.010FinishedDone
{{ 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 '—' }}{{ row.final_date or '—' }}
- - - - + + + + {% for row in suspect %} - - - - + + + {% endfor %} diff --git a/src/flask/templates/brews.html b/src/flask/templates/brews.html index 0bebe60..7640b8a 100644 --- a/src/flask/templates/brews.html +++ b/src/flask/templates/brews.html @@ -78,48 +78,52 @@
RunOGFGProblemRunOGFGProblem
+ {{ row.beer_run_id }} {{ '%.3f' % (row.og_points / 1000) }}{{ '%.3f' % (row.fg_points / 1000) }}{{ row.problem }}{{ '%.3f' % (row.og_points / 1000) }}{{ '%.3f' % (row.fg_points / 1000) }}{{ row.problem }}
- - - - - - - + + + + + + + {% for run in active_runs %} - - - + + - - - - @@ -163,41 +167,41 @@
RunNameGravityEst. ABVCommentBrewing adviceRunNameGravityEst. ABVABVCommentNoteBrewing adviceAdvice
{{ run.beer_run_id }}{{ run.canonical_name }} + {{ run.beer_run_id }}{{ run.canonical_name }} {% if run.min_sg is not none %} {{ run.min_sg_decimal }} -
{{ run.min_sg_point_scale }} points
+
{{ run.min_sg_point_scale }} points
{% else %}—{% endif %}
+ {% if run.projected_abv is not none %} {{ '%.2f' % run.projected_abv }}% {% else %} — {% endif %} + {% if run.comments %} {{ run.comments[-1] }} {% else %} — {% endif %} + {% if run.advice %} + Open
- - - - - - - - + + + + + + + + {% for run in completed_runs %} - - - + + - - - - + - diff --git a/src/flask/templates/history.html b/src/flask/templates/history.html index a6c1530..ef00867 100644 --- a/src/flask/templates/history.html +++ b/src/flask/templates/history.html @@ -84,21 +84,21 @@
RunNameOGFGABVFinishedCommentRunNameOGFGABVFinishedDoneCommentNote
{{ run.beer_run_id }}{{ run.canonical_name }} + {{ run.beer_run_id }}{{ run.canonical_name }} {% if run.og_points is not none %}{{ run.og_points }}{% else %}—{% endif %} + {% if run.fg_points is not none %}{{ run.fg_points }}{% else %}—{% endif %} + {% if run.abv is not none %} {{ '%.2f' % run.abv }}% {% else %}—{% endif %} {{ run.final_date or '—' }} + {{ run.final_date or '—' }} {% if run.comments %} {{ run.comments[-1] }} {% else %} — {% endif %} + Open
- - - - - + + + + + {% for row in outliers %} - - - - - + + + + + {% endfor %} @@ -118,45 +118,45 @@
RunNameDateReadingCommentRunNameDateReadingCommentNote
{{ row.beer_run_id }}{{ row.name }}{{ row.date or '—' }}{{ row.sg }}{{ row.comment or '—' }}{{ row.beer_run_id }}{{ row.name }}{{ row.date or '—' }}{{ row.sg }}{{ row.comment or '—' }}
- - - - - - - - - - - + + + + + + + + + + + {% for run in summaries %} - - - + + - - - - + + + - - - + + - @@ -178,29 +178,29 @@
RunNameStatusOGFGDropABVLegacyFinishedCommentRunNameStatusStatOGFGDropABVLegacyFinishedDoneCommentNote
{{ run.beer_run_id }}{{ run.canonical_name }} + {{ run.beer_run_id }}{{ run.canonical_name }} {% if run.active %} Active {% else %} Finished {% endif %} {{ run.og_points or '—' }}{{ run.fg_points or '—' }}{{ run.drop_points if run.drop_points is not none else '—' }} + {{ run.og_points or '—' }}{{ run.fg_points or '—' }}{{ run.drop_points if run.drop_points is not none else '—' }} {% if run.abv is not none %}{{ '%.2f' % run.abv }}% {% elif run.projected_abv is not none %}{{ '%.2f' % run.projected_abv }}%* {% else %}—{% endif %} {{ run.legacy_abv if run.legacy_abv is not none else '—' }}{{ run.final_date or '—' }} + {{ run.legacy_abv if run.legacy_abv is not none else '—' }}{{ run.final_date or '—' }} {% if run.comments %}{{ run.comments[-1] }}{% else %}—{% endif %} + Open
- - - - - - + + + + + + {% for row in abv_rows %} - - - - - + + + + - + {% 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"})
RunNameStandard ABVLegacy ABVOverstated byFinishedRunNameStandard ABVStdLegacy ABVOldOverstated byOverFinishedDone
{{ row.beer_run_id }}{{ row.name }}{{ '%.2f' % row.abv }}%{{ '%.2f' % row.legacy_abv }}% + {{ row.beer_run_id }}{{ row.name }}{{ '%.2f' % row.abv }}%{{ '%.2f' % row.legacy_abv }}% {% if row.delta > 0 %} +{{ '%.2f' % row.delta }} {% else %} {{ '%.2f' % row.delta }} {% endif %} {{ row.final_date or '—' }}{{ row.final_date or '—' }}