3.5.8.1
Add more labels for the day tabs
This commit is contained in:
+114
-16
@@ -77,24 +77,29 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fixed first column naming the two numbers stacked in each tab — which
|
/* Fixed first column naming every stacked row in each tab — day name, date,
|
||||||
metrics those are changes with the active profile (SunSoak/Shade, Indoors/
|
the weather/status icon, and the two numbers (which metrics those are
|
||||||
Managed, Fur/Shade, Vehicle/Windows Open), so without this the numbers are
|
changes with the active profile: SunSoak/Shade, Indoors/Managed, Fur/Shade,
|
||||||
unlabelled. Sits outside .utci-day-tabs so it never scrolls away.
|
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
|
Alignment: every label is absolutely positioned at a fixed offset from the
|
||||||
numeric block is the last thing in each one, so bottom-aligning this column
|
top of the column, matching the corresponding row inside .utci-day-tab.
|
||||||
with matching padding lands the labels on the same lines as the values —
|
Taking the labels out of normal flow is what makes this robust across
|
||||||
no need to replicate the day-name / date / icon stack above them. */
|
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 {
|
.utci-day-legend-col {
|
||||||
|
position: relative;
|
||||||
flex: 0 0 var(--day-legend-w);
|
flex: 0 0 var(--day-legend-w);
|
||||||
display: flex;
|
padding: 0 6px 0 0;
|
||||||
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;
|
|
||||||
background: #fdf8ee;
|
background: #fdf8ee;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-family: Manrope, sans-serif;
|
font-family: Manrope, sans-serif;
|
||||||
@@ -103,10 +108,76 @@
|
|||||||
overflow-wrap: break-word;
|
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
|
/* 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
|
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). */
|
boxes: 14px x 1.1 and 11px x 1.1 (see the numeric block in DayTabs.js). */
|
||||||
.utci-day-legend-main {
|
.utci-day-legend-main {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -117,8 +188,17 @@
|
|||||||
color: #6b4f2a;
|
color: #6b4f2a;
|
||||||
}
|
}
|
||||||
.utci-day-legend-sec {
|
.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;
|
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;
|
justify-content: flex-end;
|
||||||
min-height: 12px;
|
min-height: 12px;
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
@@ -135,6 +215,11 @@
|
|||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
-ms-overflow-style: none; /* old Edge */
|
-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 {
|
.utci-day-tabs::-webkit-scrollbar {
|
||||||
display: none; /* Chrome/Safari */
|
display: none; /* Chrome/Safari */
|
||||||
@@ -161,6 +246,15 @@
|
|||||||
border-left: none;
|
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) {
|
.utci-day-tab:hover:not(.active):not(.locked) {
|
||||||
filter: brightness(1.05) !important;
|
filter: brightness(1.05) !important;
|
||||||
}
|
}
|
||||||
@@ -191,6 +285,9 @@
|
|||||||
font-family: Fraunces, serif;
|
font-family: Fraunces, serif;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-size: 13px;
|
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;
|
text-transform: none;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -1100,6 +1197,7 @@
|
|||||||
possible on phones. */
|
possible on phones. */
|
||||||
.utci-day-tabs-wrap { --day-legend-w: 56px; }
|
.utci-day-tabs-wrap { --day-legend-w: 56px; }
|
||||||
.utci-day-legend-col { padding: 10px 4px 11px; letter-spacing: 0.04em; }
|
.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-main { font-size: 8px; }
|
||||||
.utci-day-legend-sec { font-size: 7px; }
|
.utci-day-legend-sec { font-size: 7px; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,15 +204,22 @@ export function DayTabs({
|
|||||||
onClick=${() => scrollDayTabs(1)}
|
onClick=${() => scrollDayTabs(1)}
|
||||||
aria-label="Scroll days right"
|
aria-label="Scroll days right"
|
||||||
type="button">›</button>
|
type="button">›</button>
|
||||||
${/* Fixed label column outside the scroller — names the two stacked
|
${/* Fixed label column outside the scroller — names every stacked row
|
||||||
numbers in every tab, since which metrics they are changes with
|
in each tab (day name, date, icon, then the two numbers, since
|
||||||
the active profile. Bottom-aligned against the tabs' numeric
|
which metrics those are changes with the active profile). Mirrors
|
||||||
block (see .utci-day-legend-col in table.css). */''}
|
the tabs' own top-down layout row for row (see .utci-day-legend-col
|
||||||
|
in table.css). */''}
|
||||||
<div class="utci-day-legend-col">
|
<div class="utci-day-legend-col">
|
||||||
|
<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>
|
<span class="utci-day-legend-main" title=${mainMetricDesc}>${mainMetricLabel}</span>
|
||||||
${(isVehicleProfile || secondaryField) && html`
|
${(isVehicleProfile || secondaryField) && html`
|
||||||
|
<span class="utci-day-legend-sep" aria-hidden="true"></span>
|
||||||
<span class="utci-day-legend-sec" title=${secondaryMetricDesc}>${secondaryMetricLabel}</span>`}
|
<span class="utci-day-legend-sec" title=${secondaryMetricDesc}>${secondaryMetricLabel}</span>`}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="utci-day-tabs" ref=${dayTabsRef}>
|
<div class="utci-day-tabs" ref=${dayTabsRef}>
|
||||||
${days.map((d, i) => {
|
${days.map((d, i) => {
|
||||||
const band = confidenceBand(i);
|
const band = confidenceBand(i);
|
||||||
@@ -378,7 +385,7 @@ export function DayTabs({
|
|||||||
>
|
>
|
||||||
${locked && html`
|
${locked && html`
|
||||||
<span style=${{ position: 'absolute', top: '3px', right: '5px', fontSize: '10px', opacity: 0.75 }}>🔒</span>`}
|
<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' }}>
|
<span class="utci-day-date" style=${{ color: '#5a3f24' }}>
|
||||||
${dDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' })}
|
${dDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' })}
|
||||||
</span>
|
</span>
|
||||||
@@ -410,12 +417,16 @@ export function DayTabs({
|
|||||||
}}>
|
}}>
|
||||||
<span style=${{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1px' }}>
|
<span style=${{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1px' }}>
|
||||||
<span style=${{ color: '#c0622a', fontWeight: 800, fontSize: '14px', lineHeight: 1.1 }}>${dayHi}°</span>
|
<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>
|
||||||
<span style=${{ width: '1px', background: 'rgba(0,0,0,0.15)', margin: '2px 0', alignSelf: 'stretch' }}></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=${{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1px' }}>
|
||||||
<span style=${{ color: '#3a6080', fontWeight: 800, fontSize: '14px', lineHeight: 1.1 }}>${dayLo}°</span>
|
<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>
|
||||||
</span>`}
|
</span>`}
|
||||||
</button>`;
|
</button>`;
|
||||||
|
|||||||
+1
-1
@@ -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.' },
|
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.' },
|
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.' },
|
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.' },
|
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.' },
|
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.' },
|
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.' },
|
||||||
|
|||||||
@@ -231,6 +231,77 @@ export function useAppState() {
|
|||||||
el.scrollBy({ left: dir * 200, behavior: 'smooth' });
|
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 ──────────────────────────────────────────────────────
|
// ── 3. PRO TIER ──────────────────────────────────────────────────────
|
||||||
// (isPro is declared near the top so useForecast can read it; see above.)
|
// (isPro is declared near the top so useForecast can read it; see above.)
|
||||||
|
|
||||||
|
|||||||
@@ -171,5 +171,16 @@
|
|||||||
"custom": 1,
|
"custom": 1,
|
||||||
"farming": 1
|
"farming": 1
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"2026-07-29": {
|
||||||
|
"visits": 32,
|
||||||
|
"profiles": {
|
||||||
|
"vehicle": 9,
|
||||||
|
"home": 9,
|
||||||
|
"basic": 7,
|
||||||
|
"showall": 1,
|
||||||
|
"pets": 3,
|
||||||
|
"alltemps": 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -70,10 +70,10 @@
|
|||||||
|
|
||||||
<link rel="preconnect" href="https://api.open-meteo.com" crossorigin />
|
<link rel="preconnect" href="https://api.open-meteo.com" crossorigin />
|
||||||
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
||||||
<link rel="stylesheet" href="./assets/sunscope.css?v=15k">
|
<link rel="stylesheet" href="./assets/sunscope.css?v=15m">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="./assets/js/main.js?v=15g"></script>
|
<script type="module" src="./assets/js/main.js?v=15i"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user