From f356954f4067367aed76d7877b042457840aacf3 Mon Sep 17 00:00:00 2001 From: Fraxle Date: Wed, 29 Jul 2026 18:35:45 +0100 Subject: [PATCH] 3.5.8.1 Add more labels for the day tabs --- assets/css/table.css | 130 ++++++++++++++++++++++++++++---- assets/js/components/DayTabs.js | 31 +++++--- assets/js/config.js | 2 +- assets/js/hooks/useAppState.js | 71 +++++++++++++++++ data/tracking.json | 11 +++ index.html | 4 +- 6 files changed, 220 insertions(+), 29 deletions(-) diff --git a/assets/css/table.css b/assets/css/table.css index 712c0d7..7386b2f 100644 --- a/assets/css/table.css +++ b/assets/css/table.css @@ -77,24 +77,29 @@ pointer-events: none; } -/* Fixed first column naming the two numbers stacked in each tab — which - metrics those are changes with the active profile (SunSoak/Shade, Indoors/ - Managed, Fur/Shade, Vehicle/Windows Open), so without this the numbers are - unlabelled. Sits outside .utci-day-tabs so it never scrolls away. +/* Fixed first column naming every stacked row in each tab — day name, date, + the weather/status icon, and the two numbers (which metrics those are + changes with the active profile: SunSoak/Shade, Indoors/Managed, Fur/Shade, + Vehicle/Windows Open) — so without this column the tabs read as unlabelled + numbers. Sits outside .utci-day-tabs so it never scrolls away. - Alignment: .utci-day-tabs stretches every tab to equal height and the - numeric block is the last thing in each one, so bottom-aligning this column - with matching padding lands the labels on the same lines as the values — - no need to replicate the day-name / date / icon stack above them. */ + Alignment: every label is absolutely positioned at a fixed offset from the + top of the column, matching the corresponding row inside .utci-day-tab. + Taking the labels out of normal flow is what makes this robust across + profiles: they no longer stack against each other, so a label that wraps + to two lines ("Managed Indoors") can't push the rows below it, and a + shorter one ("Shade Air") can't pull them up. Previously every label's + position depended on the height of the ones above it, so alignment tuned + against one profile drifted on the others. + + The offsets below are measured from the tab's own stack: + 1px border + 10px padding → day 16px → +3px date 16px → +6px icon 30px + → +2px values + The legend column has no top border of its own, hence the leading 11px. */ .utci-day-legend-col { + position: relative; flex: 0 0 var(--day-legend-w); - display: flex; - flex-direction: column; - justify-content: flex-end; - align-items: flex-end; - gap: 1px; - /* Bottom 11px = the tab's 10px padding + its 1px bottom border. */ - padding: 10px 6px 11px; + padding: 0 6px 0 0; background: #fdf8ee; text-align: right; font-family: Manrope, sans-serif; @@ -103,10 +108,76 @@ overflow-wrap: break-word; } +/* Shared anchoring for every absolutely-positioned label row. `left: 0` + + `right` (rather than a width) keeps them right-aligned against the column's + padding while still allowing long labels to wrap leftwards. */ +.utci-day-legend-day, +.utci-day-legend-date, +.utci-day-legend-icon, +.utci-day-legend-values { + position: absolute; + left: 0; + right: 6px; +} + +.utci-day-legend-day { + top: 11px; + height: 16px; + /* Explicit line-height (not the browser default "normal") so the row's + height is identical in Chrome and Firefox — those engines compute + "normal" differently for the same font and size. */ + line-height: 16px; + font-size: 8px; + font-weight: 700; + color: #9a7d5a; +} +.utci-day-legend-date { + top: 30px; + height: 16px; + line-height: 16px; + font-size: 8px; + font-weight: 700; + color: #9a7d5a; +} +.utci-day-legend-icon { + top: 52px; + height: 30px; + display: flex; + align-items: center; + justify-content: flex-end; + font-size: 8px; + font-weight: 700; + color: #9a7d5a; +} + +/* Positioning context for the two value labels. The separator and the + secondary label are pinned to fixed offsets inside it (matching the tab's + own hi/lo block) rather than stacking after the main label, so a main + label that wraps to two lines can't shove them down. */ +.utci-day-legend-values { + top: 84px; + height: 0; +} + +/* Divider between the top (warmer) and bottom (cooler) values — mirrors the + thin line added between the hi/lo numbers inside each tab. */ +.utci-day-legend-sep { + position: absolute; + top: 17px; + left: 0; + right: 0; + height: 1px; + background: rgba(0, 0, 0, 0.15); +} + /* Each label matches the weight and colour family of the value beside it, so the big/small pairing reads without a legend. Heights match the value line boxes: 14px x 1.1 and 11px x 1.1 (see the numeric block in DayTabs.js). */ .utci-day-legend-main { + position: absolute; + top: 0; + left: 0; + right: 0; display: flex; align-items: center; justify-content: flex-end; @@ -117,8 +188,17 @@ color: #6b4f2a; } .utci-day-legend-sec { + /* 20px matches the tab's secondary number, measured from the top of its + hi/lo block (15px main line + 5px separator gap). Absolute, so it holds + that offset even if the main label above it wraps. */ + position: absolute; + top: 20px; + left: 0; + right: 0; display: flex; - align-items: center; + /* Top-aligned (not centred) so the label stays level with the TOP of the + cooler/secondary value beside it even when the label text wraps. */ + align-items: flex-start; justify-content: flex-end; min-height: 12px; font-size: 8px; @@ -135,6 +215,11 @@ scroll-behavior: smooth; scrollbar-width: none; /* Firefox */ -ms-overflow-style: none; /* old Edge */ + /* Grab cursor hints drag-to-scroll, matching .utci-tbody-scroll */ + cursor: grab; +} +.utci-day-tabs:active { + cursor: grabbing; } .utci-day-tabs::-webkit-scrollbar { display: none; /* Chrome/Safari */ @@ -161,6 +246,15 @@ border-left: none; } +/* Explicit line-height (rather than the browser-default "normal") so this + row's rendered height is identical across browsers — Firefox and Chrome + compute "normal" differently for the same font/size, which threw off the + legend column's row-matching in .utci-day-legend-day (table.css). */ +.utci-day-name { + display: block; + line-height: 16px; +} + .utci-day-tab:hover:not(.active):not(.locked) { filter: brightness(1.05) !important; } @@ -191,6 +285,9 @@ font-family: Fraunces, serif; font-style: italic; font-size: 13px; + /* Explicit, not "normal" — pins the row height so it renders identically + across browsers (see .utci-day-name for why this matters). */ + line-height: 16px; text-transform: none; letter-spacing: 0; display: block; @@ -1100,6 +1197,7 @@ possible on phones. */ .utci-day-tabs-wrap { --day-legend-w: 56px; } .utci-day-legend-col { padding: 10px 4px 11px; letter-spacing: 0.04em; } + .utci-day-legend-day, .utci-day-legend-date, .utci-day-legend-icon { font-size: 7px; } .utci-day-legend-main { font-size: 8px; } .utci-day-legend-sec { font-size: 7px; } } diff --git a/assets/js/components/DayTabs.js b/assets/js/components/DayTabs.js index 768bb0d..4ec01bc 100644 --- a/assets/js/components/DayTabs.js +++ b/assets/js/components/DayTabs.js @@ -204,14 +204,21 @@ export function DayTabs({ onClick=${() => scrollDayTabs(1)} aria-label="Scroll days right" type="button">› - ${/* Fixed label column outside the scroller — names the two stacked - numbers in every tab, since which metrics they are changes with - the active profile. Bottom-aligned against the tabs' numeric - block (see .utci-day-legend-col in table.css). */''} + ${/* Fixed label column outside the scroller — names every stacked row + in each tab (day name, date, icon, then the two numbers, since + which metrics those are changes with the active profile). Mirrors + the tabs' own top-down layout row for row (see .utci-day-legend-col + in table.css). */''}
- ${mainMetricLabel} - ${(isVehicleProfile || secondaryField) && html` - ${secondaryMetricLabel}`} + Day + Date + ${isHomeOrOffice ? 'Home' : isVehicleProfile ? 'Alert' : 'Sky'} +
+ ${mainMetricLabel} + ${(isVehicleProfile || secondaryField) && html` + + ${secondaryMetricLabel}`} +
${days.map((d, i) => { @@ -378,7 +385,7 @@ export function DayTabs({ > ${locked && html` 🔒`} - ${dayName} + ${dayName} ${dDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' })} @@ -410,12 +417,16 @@ export function DayTabs({ }}> ${dayHi}° - ${shadeHi !== null && html`${shadeHi}°`} + ${shadeHi !== null && html` + + ${shadeHi}°`} ${dayLo}° - ${shadeLo !== null && html`${shadeLo}°`} + ${shadeLo !== null && html` + + ${shadeLo}°`} `} `; diff --git a/assets/js/config.js b/assets/js/config.js index d52c5e3..4ca5812 100644 --- a/assets/js/config.js +++ b/assets/js/config.js @@ -276,7 +276,7 @@ export const COL_DESCRIPTIONS = { soilT6: { title: 'Soil Temp 6 cm', desc: 'Temperature of the soil at 6 cm depth, the root zone for many crops and seedlings. Lags behind surface temperature by several hours.' }, soilM: { title: 'Soil Moisture', desc: 'Water saturation of the top 1 cm of soil as a percentage. Above 40% suggests saturated ground; below 20% indicates dry conditions.' }, concreteT: { title: 'Concrete Surface', desc: 'Estimated temperature of sun-exposed urban concrete or paving. Concrete absorbs more solar energy than grass and cannot cool itself through evaporation — surface temps can run 15–25 °C above air temperature on sunny days.' }, - vehicleT: { title: 'Vehicle Interior', desc: 'Estimated ambient cabin temperature inside a parked vehicle. Cars heat rapidly through thin body panels and glass; motorhomes and caravans are modelled as insulated occupied living spaces that warm more slowly but hold heat longer, with gain through the windscreen and rooflights and a little retained living-space warmth. Accounts for ambient wind over the bodywork, road speed, and whether the windows are open. Dangerous for children and pets above 35 °C; potentially fatal above 45 °C.' }, + vehicleT: { title: 'Interior', desc: 'Estimated ambient cabin temperature inside a parked vehicle. Cars heat rapidly through thin body panels and glass; motorhomes and caravans are modelled as insulated occupied living spaces that warm more slowly but hold heat longer, with gain through the windscreen and rooflights and a little retained living-space warmth. Accounts for ambient wind over the bodywork, road speed, and whether the windows are open. Dangerous for children and pets above 35 °C; potentially fatal above 45 °C.' }, indoorT: { title: 'Indoors', desc: 'Estimated ambient temperature inside a selected building type with windows closed and no air conditioning. Accounts for retained warmth, window solar gain, internal gains, and thermal lag without repeatedly accumulating solar heat hour after hour.' }, managedT: { title: 'Managed Indoors', desc: 'Estimated indoor temperature with curtains closed and windows opened when outdoor air is cooler than inside — the standard UK heatwave advice. Curtains block most direct solar gain; smart ventilation pulls the temperature down during cooler periods.' }, vis: { title: 'Visibility', desc: 'Horizontal visibility in kilometres, sourced from the CAMS air quality model. Values below 1 km indicate fog or very thick haze; below 10 km suggests mist, smoke, or significant pollution. Relevant for driving, flying, and photography.' }, diff --git a/assets/js/hooks/useAppState.js b/assets/js/hooks/useAppState.js index 42282c0..321babb 100644 --- a/assets/js/hooks/useAppState.js +++ b/assets/js/hooks/useAppState.js @@ -231,6 +231,77 @@ export function useAppState() { el.scrollBy({ left: dir * 200, behavior: 'smooth' }); }; + // Drag-to-scroll for the day-tab strip, mirroring the hourly table's body + // scroller (see useTableScroll). Unlike the table scroller, the draggable + // surface here IS the clickable element (each day is a