Tidy simple view card spacing and locations.
Make simple view fonts bigger
Fix quick view memory
This commit is contained in:
fraxle
2026-08-16 09:40:54 +01:00
parent df3ecdae35
commit ff8a9950f4
2 changed files with 65 additions and 29 deletions
+28 -12
View File
@@ -2410,15 +2410,23 @@
} }
.fsc-card { .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; min-width: 0;
justify-self: center;
text-align: center; text-align: center;
border: none; border: none;
background: transparent; background: transparent;
padding: 10px 8px 12px; 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; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@@ -2433,11 +2441,14 @@
z-index: 1; 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 { .fsc-time {
font-family: JetBrains Mono, monospace; font-family: Fraunces, serif;
font-size: 12px; font-size: 16px;
font-weight: 700; font-weight: 500;
color: #4a3218; color: #1e1208;
line-height: 1; line-height: 1;
} }
.fsc-card--now .fsc-time { .fsc-card--now .fsc-time {
@@ -2482,11 +2493,14 @@
} }
.fsc-meta-row { .fsc-meta-row {
font-family: Manrope, sans-serif; font-family: Manrope, sans-serif;
font-size: 10px; font-size: 12px;
font-weight: 600; font-weight: 600;
line-height: 1.2; line-height: 1.25;
color: #6b5230; 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-card--now .fsc-meta-row { opacity: 1; color: #4a3218; }
.fsc-meta-rain { color: #2a6a90; } .fsc-meta-rain { color: #2a6a90; }
@@ -2538,7 +2552,9 @@
@media (max-width: 600px) { @media (max-width: 600px) {
.fsc-card { .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; padding: 8px 4px 10px;
gap: 5px; gap: 5px;
} }
+37 -17
View File
@@ -371,12 +371,18 @@ export function UTCIForecast() {
}, []); }, []);
const [simpleTemp, setSimpleTemp] = useState('utciAdj'); 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(() => { useEffect(() => {
if (activeProfile === 'vehicle' || (activeProfile === 'outdoors' && outdoorsVariant === 'driver')) setSimpleTemp('vehicleT'); if (activeProfile === 'vehicle' || (activeProfile === 'outdoors' && outdoorsVariant === 'driver')) setSimpleTemp('vehicleT');
else if (activeProfile === 'home' || (activeProfile === 'outdoors' && outdoorsVariant === 'office')) setSimpleTemp(indoorManaged ? 'managedT' : 'indoorT'); else if (activeProfile === 'home' || (activeProfile === 'outdoors' && outdoorsVariant === 'office')) setSimpleTemp(indoorManaged ? 'managedT' : 'indoorT');
else if (activeProfile === 'pets') setSimpleTemp('furSurfaceT'); else if (activeProfile === 'pets') setSimpleTemp('furSurfaceT');
else setSimpleTemp('utciAdj'); 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'); setForecastView('table');
} }
}, [activeProfile, outdoorsVariant]); }, [activeProfile, outdoorsVariant]);
@@ -639,15 +645,18 @@ export function UTCIForecast() {
// The grid is measured in HALF-hour tracks, because a card centred on its // 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, // 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 // 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 // at width:400%). The card itself is a fixed width centred in that span,
// tracks it covers, so the grid's max-content width stays close to what // so a longer bucket buys spacing between cards rather than a fatter card.
// it was and mobile doesn't gain a load of extra horizontal scroll.
// //
// tracks = [ pad ][ hour 0 ][ hour 1 ] … [ hour H-1 ][ pad ] // tracks = [ pad ][ hour 0 ][ hour 1 ] … [ hour H-1 ][ pad ]
// pad = bucket half-hours (>= the (bucket-1)/2 h overhang, + breathing room) // pad = bucket half-hours (>= the (bucket-1)/2 h overhang, + breathing room)
// hour j -> centre at (bucket + 2j + 1) / total // hour j -> centre at (bucket + 2j + 1) / total
// card -> spans 2*bucket tracks, starting one half-hour after 2j // card -> spans 2*bucket tracks, starting one half-hour after 2j
// => centre = 2j + 1 + bucket == hour j's centre ✓ // => 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 = (() => { const fscPlot = (() => {
if (!tableRows.length) return null; if (!tableRows.length) return null;
const cards = tableRows.length; const cards = tableRows.length;
@@ -660,10 +669,16 @@ export function UTCIForecast() {
const landing = []; const landing = [];
let j = 0; let j = 0;
for (const r of tableRows) { landing.push(j); j += r.isoHours ? r.isoHours.length : 1; } 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. // Every card is a fixed FSC_CARD_W wide at every interval (see .fsc-card),
const unit = 55 / (2 * bucket); // 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 { return {
hourly, hours, bucket, landing, hours, bucket, landing,
cols: `repeat(${total}, minmax(${unit.toFixed(2)}px, 1fr))`, cols: `repeat(${total}, minmax(${unit.toFixed(2)}px, 1fr))`,
// Fraction of the track width at which hour j's data point sits. // Fraction of the track width at which hour j's data point sits.
hourAt: j2 => (bucket + 2 * j2 + 1) / total, hourAt: j2 => (bucket + 2 * j2 + 1) / total,
@@ -1349,20 +1364,25 @@ export function UTCIForecast() {
${(() => { ${(() => {
if (!fscPlot) return null; if (!fscPlot) return null;
const fscSvgW = 1000, fscSvgH = 80; 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 811am 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 // Fixed scale: bottom = Freezing band bottom (10) 10, top = Danger start (44) + 10
const fscMin = -15, fscMax = 45; const fscMin = -15, fscMax = 45;
const toY = t => fscSvgH - ((t - fscMin) / (fscMax - fscMin)) * fscSvgH; const toY = t => fscSvgH - ((t - fscMin) / (fscMax - fscMin)) * fscSvgH;
const getT = r => r[simpleTemp] ?? r.utciAdj; const getT = r => r[simpleTemp] ?? r.utciAdj;
// Same hour->x mapping the cards grid uses (see fscPlot above), so a // x of card i — the landing hour it is centred on, via the same
// card's centre and its hour's data point are the same x by // mapping that placed the card in the grid, so a point and its
// construction — every hour of the day stays on screen. // card share an x by construction.
const fscHourX = j => fscPlot.hourAt(j) * fscSvgW; const fscCardX = i => fscPlot.hourAt(fscPlot.landing[i]) * fscSvgW;
const fscStopPct = j => (fscPlot.hourAt(j) * 100).toFixed(1); const fscStopPct = i => (fscPlot.hourAt(fscPlot.landing[i]) * 100).toFixed(1);
const fscAllPts = fscSrc.map((r, j) => ({ x: fscHourX(j), y: toY(getT(r)) })); const fscAllPts = fscSrc.map((r, i) => ({ x: fscCardX(i), y: toY(getT(r)) }));
// Connector points — one per card, planted on its landing hour's data // One point per card now, so the connectors ARE the data points.
// point, which is exactly where that card is centred. const fscPts = fscAllPts;
const fscPts = fscPlot.landing.map(j => fscAllPts[Math.min(j, fscAllPts.length - 1)]);
const fscNowFlags = tableRows.map(r => r.isoHours ? r.isoHours.includes(nowLocalISO) : r.iso.slice(0, 13) === nowLocalISO); 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 // Flat runs from each edge into the first/last hour, so the fill still
// covers the inset spacer strips at both ends. // covers the inset spacer strips at both ends.