fix(ui): mobile pop-out for the advice, and tables that fit a phone (#16)
Build and Push Image / Build and push image (push) Successful in 12m4s
Build and Push Image / Build and push image (push) Successful in 12m4s
fix(ui): mobile pop-out for the advice, and tables that fit a phone (#16)
This commit was merged in pull request #16.
This commit is contained in:
+262
-17
@@ -480,6 +480,138 @@ 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 .num { font-family: var(--mono); text-align: right; white-space: nowrap; }
|
||||||
table.data .comment { color: var(--text-muted); font-size: .84rem; }
|
table.data .comment { color: var(--text-muted); font-size: .84rem; }
|
||||||
|
|
||||||
|
/* On a narrow screen the tables reflow into stacked cards.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Each row becomes a card: the header cell becomes a label above its
|
||||||
|
value, and the cells stack. */
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar__logout {
|
||||||
|
order: 2;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
order: 3;
|
||||||
|
flex: 1 0 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
/* A fade at the right edge so a clipped tab reads as
|
||||||
|
scrollable rather than broken. */
|
||||||
|
-webkit-mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent 100%);
|
||||||
|
mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tighter tabs, so all four fit on a phone without scrolling.
|
||||||
|
They still scroll if they have to. */
|
||||||
|
.nav__link {
|
||||||
|
padding: .35rem .55rem;
|
||||||
|
font-size: .8rem;
|
||||||
|
letter-spacing: .01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
overflow-x: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 */
|
/* ----------------------------------------------------------------- chips */
|
||||||
|
|
||||||
.chip {
|
.chip {
|
||||||
@@ -595,8 +727,18 @@ table.data .comment { color: var(--text-muted); font-size: .84rem; }
|
|||||||
.brew-card__name { font-weight: 650; font-size: 1.02rem; margin: 0; }
|
.brew-card__name { font-weight: 650; font-size: 1.02rem; margin: 0; }
|
||||||
.brew-card__meta { color: var(--text-muted); font-size: .82rem; font-family: var(--mono); }
|
.brew-card__meta { color: var(--text-muted); font-size: .82rem; font-family: var(--mono); }
|
||||||
.brew-card__stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: .6rem; }
|
.brew-card__stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: .6rem; }
|
||||||
|
|
||||||
|
/* The three stats sit side by side, so their captions line up. One
|
||||||
|
caption ("needs 2nd reading") is two lines and the others are one,
|
||||||
|
which left the baselines ragged. */
|
||||||
|
.brew-card__stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.brew-card__stat .stat__label { font-size: .68rem; }
|
.brew-card__stat .stat__label { font-size: .68rem; }
|
||||||
.brew-card__stat .stat__value { font-size: 1.3rem; }
|
.brew-card__stat .stat__value { font-size: 1.3rem; }
|
||||||
|
.brew-card__stat .stat__meta { margin-top: auto; }
|
||||||
.brew-card__spark { height: 46px; }
|
.brew-card__spark { height: 46px; }
|
||||||
|
|
||||||
/* The "start a new beer" tile — fills the last grid slot and gives the
|
/* The "start a new beer" tile — fills the last grid slot and gives the
|
||||||
@@ -733,27 +875,130 @@ table.data .comment { color: var(--text-muted); font-size: .84rem; }
|
|||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The advice in the table is a compact trigger that pops the full text
|
||||||
|
out on hover, focus or tap.
|
||||||
|
|
||||||
|
It used to be the text itself, clamped to four lines. The advice runs
|
||||||
|
to several hundred characters, and a cell that wide pushes the table
|
||||||
|
past the width of a phone, so the column ended up clipped off the
|
||||||
|
right edge and unreadable without sideways scrolling. */
|
||||||
.advice-cell {
|
.advice-cell {
|
||||||
max-width: 32rem;
|
position: relative;
|
||||||
|
width: 1%;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* In the table the advice is clamped to a few lines - the full text is
|
.advice-trigger {
|
||||||
on the run detail page, where there is room to read it. */
|
display: inline-flex;
|
||||||
.advice-cell .advice {
|
align-items: center;
|
||||||
margin-top: 0;
|
gap: .35rem;
|
||||||
|
padding: .25rem .55rem;
|
||||||
|
border: 1px solid var(--amber);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--amber-dim);
|
||||||
|
color: var(--amber-text);
|
||||||
|
font: inherit;
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
display: -webkit-box;
|
line-height: 1.4;
|
||||||
-webkit-line-clamp: 4;
|
cursor: pointer;
|
||||||
line-clamp: 4;
|
white-space: nowrap;
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The clamped text opens up on hover, so the whole thing is readable
|
.advice-trigger:hover,
|
||||||
without leaving the page. */
|
.advice-trigger:focus-visible {
|
||||||
.advice-cell:hover .advice,
|
background: var(--amber);
|
||||||
.advice-cell:focus-within .advice {
|
color: var(--bg);
|
||||||
-webkit-line-clamp: unset;
|
}
|
||||||
line-clamp: unset;
|
|
||||||
overflow: visible;
|
.advice-trigger--failed {
|
||||||
|
border-color: var(--danger);
|
||||||
|
background: var(--danger-dim);
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.advice-trigger__icon {
|
||||||
|
font-size: .85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The pop-out.
|
||||||
|
|
||||||
|
It is `position: fixed` and anchored to the trigger by the script,
|
||||||
|
not absolutely positioned inside the cell. The table is a scroll
|
||||||
|
container, and an absolutely positioned panel inside it was clipped
|
||||||
|
by the container's own overflow - the advice was sliced in half by
|
||||||
|
the card's edge and the bottom border was cut off.
|
||||||
|
|
||||||
|
Fixed positioning takes it out of the table entirely, so it floats
|
||||||
|
above the page and is never cut by a scroll container. */
|
||||||
|
.advice-pop {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 200;
|
||||||
|
display: none;
|
||||||
|
width: min(30rem, 92vw);
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: .8rem .9rem;
|
||||||
|
border: 1px solid var(--amber);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
box-shadow: 0 12px 32px rgba(0, 0, 0, .55);
|
||||||
|
text-align: left;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advice-pop__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: .5rem;
|
||||||
|
margin-bottom: .4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advice-pop__head .advice__label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advice-pop__close {
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 .15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advice-pop__close:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.advice-pop__text {
|
||||||
|
margin: 0;
|
||||||
|
font-size: .8125rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The script opens and closes the panel, and places it. The CSS only
|
||||||
|
decides whether it is shown. */
|
||||||
|
.advice-pop.is-open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On a phone there is no hover, so the trigger is a button the script
|
||||||
|
toggles, and the panel is pinned to the middle of the screen so it is
|
||||||
|
readable whatever the table's width. */
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.advice-cell {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On a phone the panel is centred on the screen rather than
|
||||||
|
anchored to the trigger - there is not the room to float it
|
||||||
|
beside the cell, and the trigger may be near the edge. */
|
||||||
|
.advice-pop {
|
||||||
|
top: 50% !important;
|
||||||
|
left: 50% !important;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,11 +56,187 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The brewing advice pop-out.
|
||||||
|
*
|
||||||
|
* On a desktop the panel opens on hover, which the CSS does. A
|
||||||
|
* phone has no hover, so the trigger is a button that toggles it -
|
||||||
|
* and it is a real button, so it works with a keyboard and with a
|
||||||
|
* screen reader.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* The brewing advice pop-out.
|
||||||
|
*
|
||||||
|
* The advice runs to several hundred characters. In a table cell it
|
||||||
|
* pushed the table past the width of a phone, so it is a compact
|
||||||
|
* trigger instead and the text is shown in a floating panel.
|
||||||
|
*
|
||||||
|
* The panel is position:fixed so it escapes the table's scroll
|
||||||
|
* container - absolutely positioned inside the cell it was clipped
|
||||||
|
* by the container and sliced in half by the card's edge.
|
||||||
|
*
|
||||||
|
* It opens on hover on a desktop, and on tap on a phone, which has
|
||||||
|
* no hover. The trigger is a real button, so it also works with a
|
||||||
|
* keyboard.
|
||||||
|
*/
|
||||||
|
function wireAdvicePopouts() {
|
||||||
|
var triggers = document.querySelectorAll(".advice-trigger");
|
||||||
|
for (var i = 0; i < triggers.length; i++) {
|
||||||
|
(function (trigger) {
|
||||||
|
var panel = trigger.parentNode.querySelector(".advice-pop");
|
||||||
|
if (!panel) { return; }
|
||||||
|
|
||||||
|
var closeTimer = null;
|
||||||
|
|
||||||
|
function isPhone() {
|
||||||
|
return window.matchMedia("(max-width: 720px)").matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function place() {
|
||||||
|
if (isPhone()) {
|
||||||
|
/* The CSS centres it on a phone - clear any
|
||||||
|
inline position left over from a desktop
|
||||||
|
resize. */
|
||||||
|
panel.style.top = "";
|
||||||
|
panel.style.left = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var box = trigger.getBoundingClientRect();
|
||||||
|
var gap = 10;
|
||||||
|
var height = panel.offsetHeight;
|
||||||
|
var width = panel.offsetWidth;
|
||||||
|
var spaceBelow = window.innerHeight - box.bottom - gap;
|
||||||
|
var spaceAbove = box.top - gap;
|
||||||
|
|
||||||
|
/* Prefer below; flip above when there is not enough
|
||||||
|
room and there is more room above. */
|
||||||
|
var top;
|
||||||
|
if (height <= spaceBelow || spaceBelow >= spaceAbove) {
|
||||||
|
top = box.bottom + gap;
|
||||||
|
} else {
|
||||||
|
top = Math.max(gap, box.top - height - gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep it on screen horizontally. */
|
||||||
|
var left = Math.min(box.left, window.innerWidth - width - gap);
|
||||||
|
left = Math.max(gap, left);
|
||||||
|
|
||||||
|
panel.style.top = Math.round(top) + "px";
|
||||||
|
panel.style.left = Math.round(left) + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
if (closeTimer) {
|
||||||
|
clearTimeout(closeTimer);
|
||||||
|
closeTimer = null;
|
||||||
|
}
|
||||||
|
panel.classList.add("is-open");
|
||||||
|
trigger.setAttribute("aria-expanded", "true");
|
||||||
|
place();
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
panel.classList.remove("is-open");
|
||||||
|
trigger.setAttribute("aria-expanded", "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSoon() {
|
||||||
|
/* A short delay, so the mouse can travel from the
|
||||||
|
trigger into the panel without it closing. */
|
||||||
|
if (closeTimer) { clearTimeout(closeTimer); }
|
||||||
|
closeTimer = setTimeout(function () {
|
||||||
|
closeTimer = null;
|
||||||
|
if (!panel.matches(":hover") && !trigger.matches(":hover")) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}, 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger.addEventListener("click", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (panel.classList.contains("is-open")) {
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
trigger.addEventListener("mouseenter", function () {
|
||||||
|
if (!isPhone()) { open(); }
|
||||||
|
});
|
||||||
|
trigger.addEventListener("mouseleave", closeSoon);
|
||||||
|
panel.addEventListener("mouseenter", function () {
|
||||||
|
if (closeTimer) {
|
||||||
|
clearTimeout(closeTimer);
|
||||||
|
closeTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
panel.addEventListener("mouseleave", closeSoon);
|
||||||
|
|
||||||
|
var closeButton = panel.querySelector(".advice-pop__close");
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener("click", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
close();
|
||||||
|
trigger.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keydown", function (event) {
|
||||||
|
if (event.key === "Escape" && panel.classList.contains("is-open")) {
|
||||||
|
close();
|
||||||
|
trigger.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener("click", function (event) {
|
||||||
|
if (!panel.contains(event.target) && event.target !== trigger) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", function () {
|
||||||
|
if (panel.classList.contains("is-open")) { place(); }
|
||||||
|
});
|
||||||
|
window.addEventListener("scroll", function () {
|
||||||
|
if (panel.classList.contains("is-open")) { close(); }
|
||||||
|
}, true);
|
||||||
|
})(triggers[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
function init() {
|
||||||
focusFirstEmpty();
|
focusFirstEmpty();
|
||||||
defaultDateFields();
|
defaultDateFields();
|
||||||
wireToggles();
|
wireToggles();
|
||||||
wireConfirms();
|
wireConfirms();
|
||||||
|
wireAdvicePopouts();
|
||||||
|
labelTableCells();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<div class="brew-card__meta">
|
<div class="brew-card__meta">
|
||||||
Run {{ run.beer_run_id }}
|
Run {{ run.beer_run_id }}
|
||||||
{% if run.first_date %} · since {{ run.first_date }}{% endif %}
|
{% if run.first_date %} · since {{ run.first_date }}{% endif %}
|
||||||
{% if run.reading_count == 1 %} · 1 reading{% else %} · {{ run.reading_count }} readings{% endif %}
|
{% if run.reading_count == 1 %} · 1 reading{% else %} · {{ run.reading_count }} readings{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if run.tap_number is not none %}
|
{% if run.tap_number is not none %}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
<td class="num">
|
<td class="num">
|
||||||
{% if run.min_sg is not none %}
|
{% if run.min_sg is not none %}
|
||||||
<span class="mono strong">{{ run.min_sg_decimal }}</span>
|
<span class="mono strong">{{ run.min_sg_decimal }}</span>
|
||||||
<div class="faint tiny">{{ run.min_sg_point_scale }} pts</div>
|
<div class="faint tiny">{{ run.min_sg_point_scale }} points</div>
|
||||||
{% else %}—{% endif %}
|
{% else %}—{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="num">
|
<td class="num">
|
||||||
@@ -112,9 +112,24 @@
|
|||||||
<span class="faint">—</span>
|
<span class="faint">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="comment advice-cell">
|
<td class="advice-cell">
|
||||||
{% if run.advice %}
|
{% if run.advice %}
|
||||||
<span class="advice{% if not run.advice_ok %} advice--failed{% endif %}">{{ run.advice }}</span>
|
<button type="button"
|
||||||
|
class="advice-trigger{% if not run.advice_ok %} advice-trigger--failed{% endif %}"
|
||||||
|
aria-expanded="false">
|
||||||
|
<span class="advice-trigger__icon" aria-hidden="true">✎</span>
|
||||||
|
<span class="advice-trigger__label">
|
||||||
|
{% if run.advice_ok %}Read the advice{% else %}Advice failed{% endif %}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div class="advice-pop" role="tooltip">
|
||||||
|
<div class="advice-pop__head">
|
||||||
|
<span class="advice__label">Brewing advice</span>
|
||||||
|
<button type="button" class="advice-pop__close"
|
||||||
|
aria-label="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<p class="advice-pop__text">{{ run.advice }}</p>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="faint">Thinking…</span>
|
<span class="faint">Thinking…</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -995,6 +995,35 @@ def test_the_advice_is_shown_on_the_active_beers_page(client, repo):
|
|||||||
assert "Brewing advice" in body
|
assert "Brewing advice" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_advice_in_the_table_is_a_popout_not_the_text(client, repo):
|
||||||
|
"""The advice in the table is a trigger, and the text is in a panel.
|
||||||
|
|
||||||
|
The advice is several hundred characters. In a table cell it made
|
||||||
|
the table wider than a phone, so the column was clipped off the
|
||||||
|
right edge and the advice could not be read.
|
||||||
|
"""
|
||||||
|
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", "Ale", "1042", "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 trigger is a real button, so it works with a keyboard.
|
||||||
|
assert "advice-trigger" in body
|
||||||
|
assert 'class="advice-trigger' in body
|
||||||
|
|
||||||
|
# The text lives in the panel, not in the cell.
|
||||||
|
assert "advice-pop" in body
|
||||||
|
assert "advice-pop__text" in body
|
||||||
|
|
||||||
|
# The full advice is in the page - it is just not in the cell.
|
||||||
|
assert "Warm it to 21C and wait." in body
|
||||||
|
|
||||||
|
|
||||||
def test_a_failed_advice_is_marked_as_such(client, repo):
|
def test_a_failed_advice_is_marked_as_such(client, repo):
|
||||||
"""A generation failure reads differently from real advice."""
|
"""A generation failure reads differently from real advice."""
|
||||||
repo.users.insert_one({"username": "beer", "password": "beer_pass"})
|
repo.users.insert_one({"username": "beer", "password": "beer_pass"})
|
||||||
@@ -1008,7 +1037,9 @@ def test_a_failed_advice_is_marked_as_such(client, repo):
|
|||||||
|
|
||||||
response = client.get("/brews")
|
response = client.get("/brews")
|
||||||
body = response.get_data(as_text=True)
|
body = response.get_data(as_text=True)
|
||||||
assert "advice--failed" in body
|
# A failed generation is marked differently from real advice.
|
||||||
|
assert "advice-trigger--failed" in body
|
||||||
|
assert "Advice failed" in body
|
||||||
|
|
||||||
|
|
||||||
def test_advice_is_not_generated_in_tests(client, repo):
|
def test_advice_is_not_generated_in_tests(client, repo):
|
||||||
|
|||||||
Reference in New Issue
Block a user