Add more labels for the day tabs
This commit is contained in:
Fraxle
2026-07-29 18:35:45 +01:00
parent 1acadb7519
commit f356954f40
6 changed files with 220 additions and 29 deletions
+21 -10
View File
@@ -204,14 +204,21 @@ export function DayTabs({
onClick=${() => scrollDayTabs(1)}
aria-label="Scroll days right"
type="button"></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). */''}
<div class="utci-day-legend-col">
<span class="utci-day-legend-main" title=${mainMetricDesc}>${mainMetricLabel}</span>
${(isVehicleProfile || secondaryField) && html`
<span class="utci-day-legend-sec" title=${secondaryMetricDesc}>${secondaryMetricLabel}</span>`}
<span class="utci-day-legend-day">Day</span>
<span class="utci-day-legend-date">Date</span>
<span class="utci-day-legend-icon">${isHomeOrOffice ? 'Home' : isVehicleProfile ? 'Alert' : 'Sky'}</span>
<div class="utci-day-legend-values">
<span class="utci-day-legend-main" title=${mainMetricDesc}>${mainMetricLabel}</span>
${(isVehicleProfile || secondaryField) && html`
<span class="utci-day-legend-sep" aria-hidden="true"></span>
<span class="utci-day-legend-sec" title=${secondaryMetricDesc}>${secondaryMetricLabel}</span>`}
</div>
</div>
<div class="utci-day-tabs" ref=${dayTabsRef}>
${days.map((d, i) => {
@@ -378,7 +385,7 @@ export function DayTabs({
>
${locked && html`
<span style=${{ position: 'absolute', top: '3px', right: '5px', fontSize: '10px', opacity: 0.75 }}>🔒</span>`}
${dayName}
<span class="utci-day-name">${dayName}</span>
<span class="utci-day-date" style=${{ color: '#5a3f24' }}>
${dDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' })}
</span>
@@ -410,12 +417,16 @@ export function DayTabs({
}}>
<span style=${{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1px' }}>
<span style=${{ color: '#c0622a', fontWeight: 800, fontSize: '14px', lineHeight: 1.1 }}>${dayHi}°</span>
${shadeHi !== null && html`<span style=${{ color: '#c0622a', fontWeight: 600, fontSize: '11px', lineHeight: 1.1 }}>${shadeHi}°</span>`}
${shadeHi !== null && html`
<span style=${{ width: '10px', height: '1px', background: 'rgba(0,0,0,0.15)', margin: '1px 0' }}></span>
<span style=${{ color: '#c0622a', fontWeight: 600, fontSize: '11px', lineHeight: 1.1 }}>${shadeHi}°</span>`}
</span>
<span style=${{ width: '1px', background: 'rgba(0,0,0,0.15)', margin: '2px 0', alignSelf: 'stretch' }}></span>
<span style=${{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1px' }}>
<span style=${{ color: '#3a6080', fontWeight: 800, fontSize: '14px', lineHeight: 1.1 }}>${dayLo}°</span>
${shadeLo !== null && html`<span style=${{ color: '#3a6080', fontWeight: 600, fontSize: '11px', lineHeight: 1.1 }}>${shadeLo}°</span>`}
${shadeLo !== null && html`
<span style=${{ width: '10px', height: '1px', background: 'rgba(0,0,0,0.15)', margin: '1px 0' }}></span>
<span style=${{ color: '#3a6080', fontWeight: 600, fontSize: '11px', lineHeight: 1.1 }}>${shadeLo}°</span>`}
</span>
</span>`}
</button>`;
+1 -1
View File
@@ -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 1525 °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.' },
+71
View File
@@ -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 <button>), so a
// plain mousedown/mouseup pair must still open that day - only a real drag
// (movement past a small threshold) should scroll instead of select. We
// track that with `dragged` and swallow the resulting click in capture
// phase so a drag never also fires the tab's onClick.
useEffect(() => {
const el = dayTabsRef.current;
if (!el) return;
let isDown = false;
let dragged = false;
let startX = 0;
let startScroll = 0;
const suppressClick = (e) => {
e.preventDefault();
e.stopPropagation();
el.removeEventListener('click', suppressClick, true);
};
const onMouseDown = (e) => {
if (!el.contains(e.target)) return;
if (e.button !== 0) return;
isDown = true;
dragged = false;
startX = e.clientX;
startScroll = el.scrollLeft;
};
const onMouseMove = (e) => {
if (!isDown) return;
const dx = e.clientX - startX;
if (!dragged && Math.abs(dx) > 4) {
dragged = true;
// .utci-day-tabs has scroll-behavior:smooth for the chevron/snap
// scrolls elsewhere - suspend it during drag so scrollLeft tracks
// the pointer 1:1 instead of easing behind it.
el.style.scrollBehavior = 'auto';
el.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
document.body.style.webkitUserSelect = 'none';
el.addEventListener('click', suppressClick, true);
}
if (dragged) el.scrollLeft = startScroll - dx;
};
const onMouseUp = () => {
if (!isDown) return;
isDown = false;
el.style.scrollBehavior = '';
el.style.cursor = '';
document.body.style.userSelect = '';
document.body.style.webkitUserSelect = '';
// Safety net: normally the browser's own click (fired right after
// mouseup, synchronously ahead of this timeout) hits suppressClick
// first and removes it. But if mouseup lands outside every tab or a
// click never follows for any other reason, this stops the listener
// leaking and silently swallowing the *next* real click.
if (dragged) setTimeout(() => el.removeEventListener('click', suppressClick, true), 0);
};
document.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
return () => {
document.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
el.removeEventListener('click', suppressClick, true);
};
}, [forecast]);
// ── 3. PRO TIER ──────────────────────────────────────────────────────
// (isPro is declared near the top so useForecast can read it; see above.)