Added variable temps to the simple view
Fixed height shifting on model selections
Tweaked style and added guides in simple view
Linked profiles to simple view
This commit is contained in:
fraxle
2026-06-13 10:03:54 +01:00
parent 445fba033d
commit d8465675cd
6 changed files with 231 additions and 40 deletions
+5 -4
View File
@@ -9,7 +9,7 @@
grid-template-columns: 1fr auto 1fr; /* left | dial | why */ grid-template-columns: 1fr auto 1fr; /* left | dial | why */
grid-template-areas: "hleft dial hright"; grid-template-areas: "hleft dial hright";
column-gap: 32px; column-gap: 32px;
align-items: center; align-items: stretch;
padding-bottom: 6px; padding-bottom: 6px;
margin-bottom: 6px; margin-bottom: 6px;
} }
@@ -186,8 +186,8 @@
/* The big UTCI dial in the middle of the header. /* The big UTCI dial in the middle of the header.
Change width/height to resize it. */ Change width/height to resize it. */
.scope-ring { .scope-ring {
width: 220px; width: 270px;
height: 220px; height: 270px;
display: block; display: block;
} }
@@ -231,6 +231,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
gap: 8px; gap: 8px;
} }
@@ -296,7 +297,7 @@
align-items: center; align-items: center;
gap: 2px; gap: 2px;
border-top: 1px solid #c9b08a; border-top: 1px solid #c9b08a;
width: 220px; /* match desktop/tablet dial width */ width: 270px; /* match desktop/tablet dial width */
} }
.scope-env-label { .scope-env-label {
+43 -4
View File
@@ -1398,14 +1398,12 @@
display: none; display: none;
} }
.table-main--simple { .table-main--simple {
align-self: stretch; display: block;
display: flex;
flex-direction: column;
} }
.table-main--simple .forecast-simple-wrap { .table-main--simple .forecast-simple-wrap {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; height: 420px;
border: 1.5px solid #d4c0a0; border: 1.5px solid #d4c0a0;
background: #fdf8ee; background: #fdf8ee;
} }
@@ -1423,6 +1421,10 @@
overflow-y: hidden; overflow-y: hidden;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #c9b08a transparent; scrollbar-color: #c9b08a transparent;
cursor: grab;
}
.forecast-simple-scroll:active {
cursor: grabbing;
} }
.forecast-simple-scroll::-webkit-scrollbar { .forecast-simple-scroll::-webkit-scrollbar {
height: 4px; height: 4px;
@@ -1540,6 +1542,43 @@
min-height: 60px; min-height: 60px;
} }
.fvt-model-sep {
width: 1px;
height: 18px;
background: #d4c0a0;
margin: 0 4px;
flex-shrink: 0;
}
.fsc-table-btn-wrap {
padding: 10px 16px 12px;
border-top: 1px solid #d4c0a0;
background: #fdf8ee;
flex-shrink: 0;
}
.fsc-table-btn {
display: block;
width: 100%;
padding: 10px 16px;
border: 1.5px solid #a87020;
border-radius: 6px;
background: linear-gradient(to bottom, #d4a030, #b8781a);
font-family: Manrope, sans-serif;
font-size: 14px;
font-weight: 700;
color: #fff;
letter-spacing: 0.03em;
cursor: pointer;
text-align: center;
box-shadow: 0 1px 4px rgba(160,100,20,0.22);
text-shadow: 0 1px 2px rgba(0,0,0,0.18);
}
.fsc-table-btn:hover {
background: linear-gradient(to bottom, #e8b848, #cc9030);
border-color: #c8922a;
box-shadow: 0 2px 8px rgba(200,146,42,0.28);
}
@media (max-width: 600px) { @media (max-width: 600px) {
.fsc-card { .fsc-card {
min-width: 55px; min-width: 55px;
+166 -26
View File
@@ -138,6 +138,59 @@ export function UTCIForecast() {
const searchWrapRef = useRef(null); const searchWrapRef = useRef(null);
const [colTogglesOpen, setColTogglesOpen] = useState(false); const [colTogglesOpen, setColTogglesOpen] = useState(false);
const searchInputRef = useRef(null); const searchInputRef = useRef(null);
const fscScrollRef = useRef(null);
useEffect(() => {
const el = fscScrollRef.current;
if (!el) return;
let isDown = false, startX = 0, startScroll = 0, hasDragged = false;
const onMouseDown = (e) => {
if (!el.contains(e.target) || e.button !== 0) return;
isDown = true; hasDragged = false;
startX = e.clientX; startScroll = el.scrollLeft;
document.body.style.userSelect = 'none';
document.body.style.webkitUserSelect = 'none';
};
const onMouseMove = (e) => {
if (!isDown) return;
const dx = e.clientX - startX;
if (Math.abs(dx) > 5) {
hasDragged = true;
el.style.cursor = 'grabbing';
el.scrollLeft = startScroll - dx;
}
};
const onMouseUp = () => {
if (!isDown) return;
isDown = false;
el.style.cursor = '';
document.body.style.userSelect = '';
document.body.style.webkitUserSelect = '';
};
const onClickCapture = (e) => {
if (hasDragged) { e.stopPropagation(); e.preventDefault(); hasDragged = false; }
};
document.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
el.addEventListener('click', onClickCapture, true);
return () => {
document.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
el.removeEventListener('click', onClickCapture, true);
};
}, []);
const [simpleTemp, setSimpleTemp] = useState('utciAdj');
useEffect(() => {
if (activeProfile === 'vehicle') setSimpleTemp('vehicleT');
else if (activeProfile === 'home') setSimpleTemp('indoorT');
else setSimpleTemp('utciAdj');
if (['alltemps', 'showall', 'custom', 'farming', 'construction', 'market', 'windowcleaning', 'office'].includes(activeProfile)) {
setForecastView('table');
}
}, [activeProfile]);
// Focus the input the moment the search opens. // Focus the input the moment the search opens.
useEffect(() => { useEffect(() => {
@@ -668,6 +721,65 @@ export function UTCIForecast() {
<span class="fvt-interval"> <span class="fvt-interval">
${[1, 2, 3, 4].map(n => html`<button key=${n} type="button" class=${'hour-interval-btn' + (tableInterval === n ? ' on' : '')} title=${n === 1 ? 'Every hour' : `Every ${n} hours`} onClick=${() => setTableInterval(n)}>${n}h</button>`)} ${[1, 2, 3, 4].map(n => html`<button key=${n} type="button" class=${'hour-interval-btn' + (tableInterval === n ? ' on' : '')} title=${n === 1 ? 'Every hour' : `Every ${n} hours`} onClick=${() => setTableInterval(n)}>${n}h</button>`)}
</span> </span>
${forecastView === 'simple' && (() => {
const showSolar = visibleCols.utciP;
const showVehicle = visibleCols.vehicleT;
const showIndoor = visibleCols.indoorT || visibleCols.managedT;
if (!showSolar && !showVehicle && !showIndoor) return null;
const solarOn = simpleTemp === 'utciAdj';
const vehicleOn = simpleTemp === 'vehicleT';
const indoorOn = simpleTemp === 'indoorT' || simpleTemp === 'managedT';
return html`
<span class="fvt-model-sep"></span>
${showSolar && html`
<span class=${'col-toggle-group' + (solarOn ? ' col-toggle-group--expanded' : '')}>
<${CustomSelect}
value=${utciEnv} isOn=${solarOn} grpClass="grp-felt"
hideLabel="Solar Model" hidingLabel="Solar Model"
groupedLeft=${true} isLastChild=${true}
options=${Object.entries(UTCI_ENVIRONMENTS).map(([k, v]) => ({ value: k, label: v.label }))}
onChange=${v => {
if (v === 'off') setSimpleTemp(showVehicle ? 'vehicleT' : showIndoor ? 'indoorT' : 'utciAdj');
else { setUtciEnv(v); setSimpleTemp('utciAdj'); }
}}
/>
</span>`}
${showVehicle && html`
<span class=${'col-toggle-group' + (vehicleOn ? ' col-toggle-group--expanded' : '')}>
<${CustomSelect}
value=${vehicleType} isOn=${vehicleOn} grpClass="grp-felt"
hideLabel="Vehicle" hidingLabel="Vehicle"
groupedLeft=${true} isLastChild=${!vehicleOn}
options=${Object.entries(VEHICLE_TYPES).map(([k, v]) => ({ value: k, label: v.name }))}
onChange=${v => {
if (v === 'off') setSimpleTemp(showSolar ? 'utciAdj' : showIndoor ? 'indoorT' : 'vehicleT');
else { setVehicleType(v); setSimpleTemp('vehicleT'); }
}}
/>
${vehicleOn && html`
<${VentPill} checked=${vehicleVent} onChange=${() => setVehicleVent(v => !v)}
grpClass="grp-felt" label="Ventilation"
title="Open windows significantly reduce cabin heat build-up" />`}
</span>`}
${showIndoor && html`
<span class=${'col-toggle-group' + (indoorOn ? ' col-toggle-group--expanded' : '')}>
<${CustomSelect}
value=${buildingType} isOn=${indoorOn} grpClass="grp-felt"
hideLabel="Indoors" hidingLabel="Indoors"
groupedLeft=${true} isLastChild=${!indoorOn}
options=${Object.entries(BUILDING_TYPES).map(([k, v]) => ({ value: k, label: v.name }))}
onChange=${v => {
if (v === 'off') setSimpleTemp(showSolar ? 'utciAdj' : showVehicle ? 'vehicleT' : 'indoorT');
else { setBuildingType(v); setIndoorMode('on'); setSimpleTemp(indoorManaged ? 'managedT' : 'indoorT'); }
}}
/>
${indoorOn && html`
<${VentPill} checked=${indoorManaged} onChange=${() => { const next = !indoorManaged; setIndoorManaged(next); setSimpleTemp(next ? 'managedT' : 'indoorT'); }}
grpClass="grp-felt" label="Managed"
title="Curtains closed by day, windows open when cooler outside" />`}
</span>`}
`;
})()}
<span class="col-toggles-label">Columns:</span> <span class="col-toggles-label">Columns:</span>
<button class=${'col-toggles-edit-btn' + (colTogglesOpen ? ' col-toggles-edit-btn--open' : '')} onClick=${() => setColTogglesOpen(v => !v)}> <button class=${'col-toggles-edit-btn' + (colTogglesOpen ? ' col-toggles-edit-btn--open' : '')} onClick=${() => setColTogglesOpen(v => !v)}>
<span class="col-toggles-edit-btn-label">${colTogglesOpen ? 'Done' : 'Edit columns'}</span> <span class="col-toggles-edit-btn-label">${colTogglesOpen ? 'Done' : 'Edit columns'}</span>
@@ -840,17 +952,18 @@ export function UTCIForecast() {
</div> </div>
<div class="forecast-simple-wrap"> <div class="forecast-simple-wrap">
<div class="forecast-simple-scroll"> <div class="forecast-simple-scroll" ref=${fscScrollRef}>
<div class="forecast-simple-inner"> <div class="forecast-simple-inner">
<div class="forecast-simple-cards" style=${{ gridTemplateColumns: `repeat(${tableRows.length}, minmax(55px, 1fr))` }}> <div class="forecast-simple-cards" style=${{ gridTemplateColumns: `repeat(${tableRows.length}, minmax(55px, 1fr))` }}>
${tableRows.map(r => { ${tableRows.map(r => {
const cat = utciCategory(r.utciAdj); const dispTemp = r[simpleTemp] ?? r.utciAdj;
const cat = utciCategory(dispTemp);
const h24s = parseInt(r.iso.slice(11, 13), 10); const h24s = parseInt(r.iso.slice(11, 13), 10);
const localHHMMs = h24s === 0 ? '12am' : h24s < 12 ? `${h24s}am` : h24s === 12 ? '12pm' : `${h24s - 12}pm`; const localHHMMs = h24s === 0 ? '12am' : h24s < 12 ? `${h24s}am` : h24s === 12 ? '12pm' : `${h24s - 12}pm`;
const isNow = r.isoHours ? r.isoHours.includes(nowLocalISO) : r.iso.slice(0, 13) === nowLocalISO; const isNow = r.isoHours ? r.isoHours.includes(nowLocalISO) : r.iso.slice(0, 13) === nowLocalISO;
const domIcon = (() => { const domIcon = (() => {
if (r.precipProb > 20 && (r.precip > 0 || r.snow > 0)) if (r.precipProb > 20 && (r.precip > 0 || r.snow > 0))
return html`<${PrecipIcon} precip=${r.precip} snow=${r.snow} size=${42} />`; return html`<${PrecipIcon} precip=${r.precip} snow=${r.snow} size=${42} filled=${true} />`;
if (r.cloudCat && r.cloudCat !== 'clear') if (r.cloudCat && r.cloudCat !== 'clear')
return html`<${CloudIcon} category=${r.cloudCat} elev=${r.elev} dt=${r.dt} size=${42} filled=${true} />`; return html`<${CloudIcon} category=${r.cloudCat} elev=${r.elev} dt=${r.dt} size=${42} filled=${true} />`;
return null; return null;
@@ -868,7 +981,7 @@ export function UTCIForecast() {
<span class="fsc-meta-row fsc-meta-wind">${windMph}mph</span> <span class="fsc-meta-row fsc-meta-wind">${windMph}mph</span>
<span class="fsc-meta-row fsc-meta-dir">${r.compass ? r.compass.label : '—'}</span> <span class="fsc-meta-row fsc-meta-dir">${r.compass ? r.compass.label : '—'}</span>
</div> </div>
<div class="fsc-temp">${Math.round(r.utciAdj)}°</div> <div class="fsc-temp">${Math.round(dispTemp)}°</div>
<div class="fsc-feel" style=${{background: cat.bg, color: cat.fg}}>${cat.label}</div> <div class="fsc-feel" style=${{background: cat.bg, color: cat.fg}}>${cat.label}</div>
</div> </div>
`; `;
@@ -876,28 +989,41 @@ export function UTCIForecast() {
</div> </div>
${(() => { ${(() => {
if (!tableRows.length) return null; if (!tableRows.length) return null;
const fscTemps = tableRows.map(r => r.utciAdj); const fscN = tableRows.length;
const fscNowFlags = tableRows.map(r => r.isoHours ? r.isoHours.includes(nowLocalISO) : r.iso.slice(0, 13) === nowLocalISO);
const fscMin = Math.min(...fscTemps) - 2;
const fscMax = Math.max(...fscTemps) + 2;
const fscN = fscTemps.length;
const fscSvgW = 1000, fscSvgH = 80; const fscSvgW = 1000, fscSvgH = 80;
const fscCardW = fscSvgW / fscN; const fscCardW = fscSvgW / fscN;
// Card-centre points — used for connector lines and the stroke curve // Use full 1-hour data for the smooth curve, tableRows for connectors/gradient
const fscPts = fscTemps.map((t, i) => ({ const fscSrc = visible.length > fscN ? visible : tableRows;
x: (i + 0.5) * fscCardW, // Fixed scale: bottom = Freezing band bottom (10) 10, top = Danger start (44) + 10
y: fscSvgH - ((t - fscMin) / (fscMax - fscMin)) * 44 - 6, const fscMin = -15, fscMax = 45;
const toY = t => fscSvgH - ((t - fscMin) / (fscMax - fscMin)) * fscSvgH;
const getT = r => r[simpleTemp] ?? r.utciAdj;
// 1-hour points for the smooth curve — evenly spread across SVG width
const fscAllPts = fscSrc.map((r, j) => ({
x: (j + 0.5) * fscSvgW / fscSrc.length,
y: toY(getT(r)),
})); }));
// Extended points (edge-anchored) used only for the gradient fill // Connector points — x centred under card column, y at the exact point
const fscFillPts = [{ x: 0, y: fscPts[0].y }, ...fscPts, { x: fscSvgW, y: fscPts[fscN - 1].y }]; // the 1-hour bezier curve passes through at that x.
// Stroke path through card centres // For a bucket of size n starting at visible index jFirst, the card
let fscLine = `M ${fscPts[0].x},${fscPts[0].y}`; // centre x lands at the bezier midpoint between visible[jFirst+(n-1)/2 floor]
for (let i = 1; i < fscPts.length; i++) { // and visible[jFirst+(n-1)/2 ceil], so y = lerp of those two neighbours.
const p0 = fscPts[i - 1], p1 = fscPts[i]; let fscVj = 0;
const cpx = (p0.x + p1.x) / 2; const fscPts = tableRows.map((r, i) => {
fscLine += ` C ${cpx},${p0.y} ${cpx},${p1.y} ${p1.x},${p1.y}`; const bucketLen = r.isoHours ? r.isoHours.length : 1;
} const jFirst = fscVj;
// Fill path through edge-anchored points fscVj += bucketLen;
const x = (i + 0.5) * fscCardW;
const ctr = (bucketLen - 1) / 2;
const jLow = Math.min(jFirst + Math.floor(ctr), fscSrc.length - 1);
const jHigh = Math.min(jFirst + Math.ceil(ctr), fscSrc.length - 1);
const frac = ctr - Math.floor(ctr);
const tCtr = getT(fscSrc[jLow]) * (1 - frac) + getT(fscSrc[jHigh]) * frac;
return { x, y: toY(tCtr) };
});
const fscNowFlags = tableRows.map(r => r.isoHours ? r.isoHours.includes(nowLocalISO) : r.iso.slice(0, 13) === nowLocalISO);
// Extended fill points (edge-anchored) for the gradient area
const fscFillPts = [{ x: 0, y: fscAllPts[0].y }, ...fscAllPts, { x: fscSvgW, y: fscAllPts[fscAllPts.length - 1].y }];
let fscFillLine = `M ${fscFillPts[0].x},${fscFillPts[0].y}`; let fscFillLine = `M ${fscFillPts[0].x},${fscFillPts[0].y}`;
for (let i = 1; i < fscFillPts.length; i++) { for (let i = 1; i < fscFillPts.length; i++) {
const p0 = fscFillPts[i - 1], p1 = fscFillPts[i]; const p0 = fscFillPts[i - 1], p1 = fscFillPts[i];
@@ -909,12 +1035,20 @@ export function UTCIForecast() {
<svg class="fsc-temp-curve" viewBox="0 0 ${fscSvgW} ${fscSvgH}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg"> <svg class="fsc-temp-curve" viewBox="0 0 ${fscSvgW} ${fscSvgH}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<linearGradient id="fsc-grad" x1="0" y1="0" x2="1" y2="0" gradientUnits="objectBoundingBox"> <linearGradient id="fsc-grad" x1="0" y1="0" x2="1" y2="0" gradientUnits="objectBoundingBox">
${fscTemps.map((t, i) => { ${fscSrc.map((r, j) => {
const fc = utciCategory(t); const fc = utciCategory(getT(r));
return html`<stop key=${i} offset=${`${((i + 0.5) / fscN * 100).toFixed(1)}%`} stop-color=${fc.bg} />`; return html`<stop key=${j} offset=${`${((j + 0.5) / fscSrc.length * 100).toFixed(1)}%`} stop-color=${fc.bg} />`;
})} })}
</linearGradient> </linearGradient>
</defs> </defs>
${[[0, '#4a90c4'], [20, '#4a9e6a']].map(([t, color]) => {
const gy = toY(t);
return html`
<line key=${'g'+t} x1="0" y1=${gy} x2=${fscSvgW} y2=${gy}
stroke=${color} stroke-width="1" opacity="0.35"
vector-effect="non-scaling-stroke" />
`;
})}
${fscPts.map((pt, i) => { ${fscPts.map((pt, i) => {
const isNow = fscNowFlags[i]; const isNow = fscNowFlags[i];
return html` return html`
@@ -922,6 +1056,7 @@ export function UTCIForecast() {
stroke=${isNow ? '#c8922a' : '#b09870'} stroke=${isNow ? '#c8922a' : '#b09870'}
stroke-width="1" stroke-width="1"
opacity=${isNow ? '1' : '0.55'} opacity=${isNow ? '1' : '0.55'}
vector-effect="non-scaling-stroke"
/> />
`; `;
})} })}
@@ -931,6 +1066,11 @@ export function UTCIForecast() {
})()} })()}
</div> </div>
</div> </div>
<div class="fsc-table-btn-wrap">
<button type="button" class="fsc-table-btn" onClick=${() => setForecastView('table')}>
View Detailed Hourly Forecast
</button>
</div>
</div> </div>
<div class=${`utci-table-wrap${tableCanScrollLeft ? ' scroll-fade-left' : ''}${tableCanScrollRight ? ' scroll-fade-right' : ''}${showUnits ? '' : ' no-units'}`} ref=${tableWrapRef}> <div class=${`utci-table-wrap${tableCanScrollLeft ? ' scroll-fade-left' : ''}${tableCanScrollRight ? ' scroll-fade-right' : ''}${showUnits ? '' : ' no-units'}`} ref=${tableWrapRef}>
+5 -5
View File
@@ -464,13 +464,13 @@ export function CloudIcon({ category, size = 30, elev = 90, dt = new Date(), fil
${mist(size * 0.18, brass)} ${mist(size * 0.18, brass)}
${line(size * 0.18, size * 0.56, size * 0.60, size * 0.56, muted, sw * 0.7, 0.6)}`} ${line(size * 0.18, size * 0.56, size * 0.60, size * 0.56, muted, sw * 0.7, 0.6)}`}
${category === 'scattered' && html` ${category === 'scattered' && html`
<path d=${cloudPath(0, -size * 0.2425, 0.82)} fill=${filled ? 'rgba(255,255,255,0.88)' : 'none'} stroke=${ink} <path d=${cloudPath(0, -size * 0.2425, 0.82)} fill=${filled ? 'rgba(255,255,255,0.88)' : 'none'} stroke=${filled ? 'none' : ink}
stroke-width=${sw} stroke-linecap=${cap} stroke-linejoin=${join} /> stroke-width=${sw} stroke-linecap=${cap} stroke-linejoin=${join} />
${!filled && line(size * 0.20, size * 0.58, size * 0.48, size * 0.58, brass, sw * 0.75, 0.78)}`} ${!filled && line(size * 0.20, size * 0.58, size * 0.48, size * 0.58, brass, sw * 0.75, 0.78)}`}
${category === 'overcast' && html` ${category === 'overcast' && html`
<path d=${cloudPath(-size * 0.02, -size * 0.2275, 0.70)} fill=${filled ? 'rgba(220,230,240,0.72)' : 'none'} stroke=${filled ? 'rgba(180,200,220,0.6)' : muted} <path d=${cloudPath(-size * 0.02, -size * 0.2275, 0.70)} fill=${filled ? 'rgba(220,230,240,0.72)' : 'none'} stroke=${filled ? 'none' : muted}
stroke-width=${sw * 0.9} stroke-linecap=${cap} stroke-linejoin=${join} opacity="0.82" /> stroke-width=${sw * 0.9} stroke-linecap=${cap} stroke-linejoin=${join} opacity="0.82" />
<path d=${cloudPath(0, -size * 0.25, 0.88)} fill=${filled ? 'rgba(255,255,255,0.92)' : 'none'} stroke=${ink} <path d=${cloudPath(0, -size * 0.25, 0.88)} fill=${filled ? 'rgba(255,255,255,0.92)' : 'none'} stroke=${filled ? 'none' : ink}
stroke-width=${sw} stroke-linecap=${cap} stroke-linejoin=${join} />`} stroke-width=${sw} stroke-linecap=${cap} stroke-linejoin=${join} />`}
</svg>`; </svg>`;
} }
@@ -1180,7 +1180,7 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g
// heavy snow (- 0.5cm/h) - 2 flakes // heavy snow (- 0.5cm/h) - 2 flakes
// mixed - 1 drop + 1 flake // mixed - 1 drop + 1 flake
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function PrecipIcon({ precip = 0, snow = 0, size = 28 }) { export function PrecipIcon({ precip = 0, snow = 0, size = 28, filled = false }) {
const r = size / 2; const r = size / 2;
const hasRain = precip > 0; const hasRain = precip > 0;
const hasSnow = snow > 0; const hasSnow = snow > 0;
@@ -1196,7 +1196,7 @@ export function PrecipIcon({ precip = 0, snow = 0, size = 28 }) {
C ${size*0.86} ${size*0.36}, ${size*0.88} ${size*0.20}, ${size*0.72} ${size*0.19} C ${size*0.86} ${size*0.36}, ${size*0.88} ${size*0.20}, ${size*0.72} ${size*0.19}
C ${size*0.66} ${size*0.04}, ${size*0.42} ${size*0.02}, ${size*0.34} ${size*0.17} C ${size*0.66} ${size*0.04}, ${size*0.42} ${size*0.02}, ${size*0.34} ${size*0.17}
C ${size*0.22} ${size*0.14}, ${size*0.14} ${size*0.24}, ${size*0.18} ${size*0.36} Z`} C ${size*0.22} ${size*0.14}, ${size*0.14} ${size*0.24}, ${size*0.18} ${size*0.36} Z`}
fill="none" stroke=${ink} stroke-width=${sw} stroke-linecap="round" stroke-linejoin="round" />`; fill=${filled ? 'rgba(255,255,255,0.88)' : 'none'} stroke=${filled ? 'none' : ink} stroke-width=${sw} stroke-linecap="round" stroke-linejoin="round" />`;
const rainStroke = (cx, cy, heavy = false) => html` const rainStroke = (cx, cy, heavy = false) => html`
<line x1=${cx + size * 0.04} y1=${cy} x2=${cx - size * 0.04} y2=${cy + size * 0.18} <line x1=${cx + size * 0.04} y1=${cy} x2=${cx - size * 0.04} y2=${cy + size * 0.18}
stroke=${rain} stroke-width=${heavy ? sw * 1.1 : sw} stroke=${rain} stroke-width=${heavy ? sw * 1.1 : sw}
+1 -1
View File
@@ -441,7 +441,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
...(show('soilT') ? [{ ...(show('soilT') ? [{
icon: '🌱', icon: '🌱',
label: 'Soil warms to 10°C by', label: 'Soil warms to 10°C by',
value: soilWarmRow ? hhmm(soilWarmRow.iso) : 'Not today', value: soilWarmRow ? (hhmm(soilWarmRow.iso) === '00:00' ? 'All Day' : hhmm(soilWarmRow.iso)) : 'Not today',
alert: !soilWarmRow, alert: !soilWarmRow,
}] : []), }] : []),
...(show('precipProb') ? [rainItem] : []), ...(show('precipProb') ? [rainItem] : []),
+11
View File
@@ -40,5 +40,16 @@
"custom": 1, "custom": 1,
"farming": 2 "farming": 2
} }
},
"2026-06-13": {
"visits": 36,
"profiles": {
"showall": 4,
"alltemps": 5,
"vehicle": 20,
"basic": 18,
"home": 29,
"farming": 2
}
} }
} }