From ff8a9950f492dc18d6d0234be0c7a53e13f8e637 Mon Sep 17 00:00:00 2001 From: fraxle Date: Sun, 16 Aug 2026 09:40:54 +0100 Subject: [PATCH] 5.1.1 Tidy simple view card spacing and locations. Make simple view fonts bigger Fix quick view memory --- assets/css/table.css | 40 ++++++++++++++++++++++---------- assets/js/app.js | 54 ++++++++++++++++++++++++++++++-------------- 2 files changed, 65 insertions(+), 29 deletions(-) diff --git a/assets/css/table.css b/assets/css/table.css index 3198f9b..95d04f8 100644 --- a/assets/css/table.css +++ b/assets/css/table.css @@ -2410,15 +2410,23 @@ } .fsc-card { + /* One fixed width at every hour interval — a 4h card is not a wider card, + just a card with more space either side of it. It spans 2*bucket grid + tracks (see fscPlot in app.js) and is centred within that span, so it + stays over its landing hour and keeps lining up with the curve below + however wide the span gets. Must match FSC_CARD_W in app.js. */ + width: 92px; + /* Kept alongside the fixed width: .fsc-feel is nowrap, so a long band label + ("Very Strong Heat Stress") gives the card a ~146px min-content. Left to + the automatic minimum that can push the tracks it spans wider than their + neighbours', and the grid stops being uniform — which is the one thing + the connector-line maths (hourAt in app.js) assumes. */ min-width: 0; + justify-self: center; text-align: center; border: none; background: transparent; padding: 10px 8px 12px; - /* 2px each side = 4px between neighbouring cards. Done with margin rather - than a grid gap so the cards stay centred on their landing hour and keep - lining up with the curve below. */ - margin: 0 2px; display: flex; flex-direction: column; align-items: center; @@ -2433,11 +2441,14 @@ z-index: 1; } +/* Same display serif as the hour cell in the Detailed and Table views + (.utci-time) — the time reads as the same thing in all three. Now the + cards are a fixed 92px this can carry the full 16px too. */ .fsc-time { - font-family: JetBrains Mono, monospace; - font-size: 12px; - font-weight: 700; - color: #4a3218; + font-family: Fraunces, serif; + font-size: 16px; + font-weight: 500; + color: #1e1208; line-height: 1; } .fsc-card--now .fsc-time { @@ -2482,11 +2493,14 @@ } .fsc-meta-row { font-family: Manrope, sans-serif; - font-size: 10px; + font-size: 12px; font-weight: 600; - line-height: 1.2; + line-height: 1.25; color: #6b5230; - opacity: 0.8; + /* Was 0.8 at 10px, which read as grey-on-cream mush. The wider cards have + the room, and these three lines are the only numbers on the card besides + the temperature — they should be legible, not decorative. */ + opacity: 0.9; } .fsc-card--now .fsc-meta-row { opacity: 1; color: #4a3218; } .fsc-meta-rain { color: #2a6a90; } @@ -2538,7 +2552,9 @@ @media (max-width: 600px) { .fsc-card { - min-width: 55px; + /* Width holds at 92px on mobile too — the row scrolls horizontally, so + narrowing the cards buys nothing and costs legibility. Only the inner + padding tightens. */ padding: 8px 4px 10px; gap: 5px; } diff --git a/assets/js/app.js b/assets/js/app.js index 1f0253b..9c697ae 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -371,12 +371,18 @@ export function UTCIForecast() { }, []); const [simpleTemp, setSimpleTemp] = useState('utciAdj'); + // Skips the very first run so a page refresh keeps the saved Simple / + // Table / Detailed choice — the profile-driven jump to the table below + // is meant for an actual profile *switch*, not for restoring state. + const profileViewFirstRun = useRef(true); useEffect(() => { if (activeProfile === 'vehicle' || (activeProfile === 'outdoors' && outdoorsVariant === 'driver')) setSimpleTemp('vehicleT'); else if (activeProfile === 'home' || (activeProfile === 'outdoors' && outdoorsVariant === 'office')) setSimpleTemp(indoorManaged ? 'managedT' : 'indoorT'); else if (activeProfile === 'pets') setSimpleTemp('furSurfaceT'); else setSimpleTemp('utciAdj'); - if (['alltemps', 'showall', 'custom', 'farming', 'construction', 'market', 'windowcleaning', 'office'].includes(activeProfile)) { + if (profileViewFirstRun.current) { + profileViewFirstRun.current = false; + } else if (['alltemps', 'showall', 'custom', 'farming', 'construction', 'market', 'windowcleaning', 'office'].includes(activeProfile)) { setForecastView('table'); } }, [activeProfile, outdoorsVariant]); @@ -639,15 +645,18 @@ export function UTCIForecast() { // The grid is measured in HALF-hour tracks, because a card centred on its // landing hour starts on a half-hour boundary. All tracks are identical, // and each card SPANS 2*bucket of them (rather than sitting in one track - // at width:400%) — spanning divides a card's intrinsic width across the - // tracks it covers, so the grid's max-content width stays close to what - // it was and mobile doesn't gain a load of extra horizontal scroll. + // at width:400%). The card itself is a fixed width centred in that span, + // so a longer bucket buys spacing between cards rather than a fatter card. // // tracks = [ pad ][ hour 0 ][ hour 1 ] … [ hour H-1 ][ pad ] // pad = bucket half-hours (>= the (bucket-1)/2 h overhang, + breathing room) // hour j -> centre at (bucket + 2j + 1) / total // card -> spans 2*bucket tracks, starting one half-hour after 2j // => centre = 2j + 1 + bucket == hour j's centre ✓ + // Quick-view card width, and the minimum clearance between two cards. Kept + // here rather than only in CSS because the grid track sizing is derived from + // them; .fsc-card sets the same width. + const FSC_CARD_W = 92, FSC_CARD_GAP = 4; const fscPlot = (() => { if (!tableRows.length) return null; const cards = tableRows.length; @@ -660,10 +669,16 @@ export function UTCIForecast() { const landing = []; let j = 0; for (const r of tableRows) { landing.push(j); j += r.isoHours ? r.isoHours.length : 1; } - // A card spans 2*bucket tracks, so this keeps its 55px minimum. - const unit = 55 / (2 * bucket); + // Every card is a fixed FSC_CARD_W wide at every interval (see .fsc-card), + // centred in the 2*bucket tracks it spans. So the tracks only have to be + // wide enough that neighbouring cards clear each other: a span of at least + // card + gutter. At 1h that is the tight case; at 3h/4h the same rule + // leaves a wider gap between cards, which is what the extra hours per card + // should look like. The 1fr half of the minmax lets the gaps grow further + // on a wide screen — the cards never do. + const unit = (FSC_CARD_W + FSC_CARD_GAP) / (2 * bucket); return { - hourly, hours, bucket, landing, + hours, bucket, landing, cols: `repeat(${total}, minmax(${unit.toFixed(2)}px, 1fr))`, // Fraction of the track width at which hour j's data point sits. hourAt: j2 => (bucket + 2 * j2 + 1) / total, @@ -1349,20 +1364,25 @@ export function UTCIForecast() { ${(() => { if (!fscPlot) return null; const fscSvgW = 1000, fscSvgH = 80; - const fscSrc = fscPlot.hourly; + // The curve plots the SAME rows the cards do. It used to plot the + // raw hourly series instead, which agrees with the cards at 1h and + // diverges at every longer interval: a card shows its bucket's + // aggregate, so an "8am 24°" card (the 8–11am mean) sat above a + // curve drawn at 8am's own 14.5°, and its connector pointed at a + // height the card never claimed. One series, one number. + const fscSrc = tableRows; // Fixed scale: bottom = Freezing band bottom (−10) − 10, top = Danger start (44) + 10 const fscMin = -15, fscMax = 45; const toY = t => fscSvgH - ((t - fscMin) / (fscMax - fscMin)) * fscSvgH; const getT = r => r[simpleTemp] ?? r.utciAdj; - // Same hour->x mapping the cards grid uses (see fscPlot above), so a - // card's centre and its hour's data point are the same x by - // construction — every hour of the day stays on screen. - const fscHourX = j => fscPlot.hourAt(j) * fscSvgW; - const fscStopPct = j => (fscPlot.hourAt(j) * 100).toFixed(1); - const fscAllPts = fscSrc.map((r, j) => ({ x: fscHourX(j), y: toY(getT(r)) })); - // Connector points — one per card, planted on its landing hour's data - // point, which is exactly where that card is centred. - const fscPts = fscPlot.landing.map(j => fscAllPts[Math.min(j, fscAllPts.length - 1)]); + // x of card i — the landing hour it is centred on, via the same + // mapping that placed the card in the grid, so a point and its + // card share an x by construction. + const fscCardX = i => fscPlot.hourAt(fscPlot.landing[i]) * fscSvgW; + const fscStopPct = i => (fscPlot.hourAt(fscPlot.landing[i]) * 100).toFixed(1); + const fscAllPts = fscSrc.map((r, i) => ({ x: fscCardX(i), y: toY(getT(r)) })); + // One point per card now, so the connectors ARE the data points. + const fscPts = fscAllPts; const fscNowFlags = tableRows.map(r => r.isoHours ? r.isoHours.includes(nowLocalISO) : r.iso.slice(0, 13) === nowLocalISO); // Flat runs from each edge into the first/last hour, so the fill still // covers the inset spacer strips at both ends.