3.0 Beta 1
New simple mode display with graph. First render and scaling test.
This commit is contained in:
+103
-1
@@ -100,6 +100,7 @@ export function UTCIForecast() {
|
||||
restoreOpen, openRestore, closeRestore,
|
||||
showUnits, toggleShowUnits,
|
||||
tableInterval, setTableInterval,
|
||||
forecastView, setForecastView,
|
||||
activityOptions, placeOptions, workOptions,
|
||||
activityValue, activityLabel, placeValue, placeLabel, workValue, workLabel,
|
||||
skinType, setSkinType,
|
||||
@@ -658,8 +659,15 @@ export function UTCIForecast() {
|
||||
|
||||
|
||||
<div class="table-with-rail">
|
||||
<div class="table-main">
|
||||
<div class=${'table-main' + (forecastView === 'simple' ? ' table-main--simple' : '')}>
|
||||
<div class="col-toggles">
|
||||
<span class="forecast-view-toggle">
|
||||
<button type="button" class=${'fvt-btn' + (forecastView === 'simple' ? ' on' : '')} onClick=${() => setForecastView('simple')}>Simple</button>
|
||||
<button type="button" class=${'fvt-btn' + (forecastView === 'table' ? ' on' : '')} onClick=${() => setForecastView('table')}>Table</button>
|
||||
</span>
|
||||
<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>`)}
|
||||
</span>
|
||||
<span class="col-toggles-label">Columns:</span>
|
||||
<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>
|
||||
@@ -831,6 +839,100 @@ export function UTCIForecast() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="forecast-simple-wrap">
|
||||
<div class="forecast-simple-scroll">
|
||||
<div class="forecast-simple-inner">
|
||||
<div class="forecast-simple-cards" style=${{ gridTemplateColumns: `repeat(${tableRows.length}, minmax(55px, 1fr))` }}>
|
||||
${tableRows.map(r => {
|
||||
const cat = utciCategory(r.utciAdj);
|
||||
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 isNow = r.isoHours ? r.isoHours.includes(nowLocalISO) : r.iso.slice(0, 13) === nowLocalISO;
|
||||
const domIcon = (() => {
|
||||
if (r.precipProb > 20 && (r.precip > 0 || r.snow > 0))
|
||||
return html`<${PrecipIcon} precip=${r.precip} snow=${r.snow} size=${42} />`;
|
||||
if (r.cloudCat && r.cloudCat !== 'clear')
|
||||
return html`<${CloudIcon} category=${r.cloudCat} elev=${r.elev} dt=${r.dt} size=${42} filled=${true} />`;
|
||||
return null;
|
||||
})();
|
||||
const windMph = Math.round((r.gust ?? r.va) * 2.237);
|
||||
return html`
|
||||
<div key=${r.iso} class=${'fsc-card' + (isNow ? ' fsc-card--now' : '')}>
|
||||
<div class="fsc-time">${localHHMMs}</div>
|
||||
<div class="fsc-scope-wrap">
|
||||
<${SkyScope} elev=${r.elev} dt=${r.dt} glob=${r.glob} size=${42} />
|
||||
${domIcon && html`<div class="fsc-wx-overlay">${domIcon}</div>`}
|
||||
</div>
|
||||
<div class="fsc-meta">
|
||||
<span class="fsc-meta-row fsc-meta-rain">${r.precipProb}%</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>
|
||||
</div>
|
||||
<div class="fsc-temp">${Math.round(r.utciAdj)}°</div>
|
||||
<div class="fsc-feel" style=${{background: cat.bg, color: cat.fg}}>${cat.label}</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${(() => {
|
||||
if (!tableRows.length) return null;
|
||||
const fscTemps = tableRows.map(r => r.utciAdj);
|
||||
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 fscCardW = fscSvgW / fscN;
|
||||
// Card-centre points — used for connector lines and the stroke curve
|
||||
const fscPts = fscTemps.map((t, i) => ({
|
||||
x: (i + 0.5) * fscCardW,
|
||||
y: fscSvgH - ((t - fscMin) / (fscMax - fscMin)) * 44 - 6,
|
||||
}));
|
||||
// Extended points (edge-anchored) used only for the gradient fill
|
||||
const fscFillPts = [{ x: 0, y: fscPts[0].y }, ...fscPts, { x: fscSvgW, y: fscPts[fscN - 1].y }];
|
||||
// Stroke path through card centres
|
||||
let fscLine = `M ${fscPts[0].x},${fscPts[0].y}`;
|
||||
for (let i = 1; i < fscPts.length; i++) {
|
||||
const p0 = fscPts[i - 1], p1 = fscPts[i];
|
||||
const cpx = (p0.x + p1.x) / 2;
|
||||
fscLine += ` C ${cpx},${p0.y} ${cpx},${p1.y} ${p1.x},${p1.y}`;
|
||||
}
|
||||
// Fill path through edge-anchored points
|
||||
let fscFillLine = `M ${fscFillPts[0].x},${fscFillPts[0].y}`;
|
||||
for (let i = 1; i < fscFillPts.length; i++) {
|
||||
const p0 = fscFillPts[i - 1], p1 = fscFillPts[i];
|
||||
const cpx = (p0.x + p1.x) / 2;
|
||||
fscFillLine += ` C ${cpx},${p0.y} ${cpx},${p1.y} ${p1.x},${p1.y}`;
|
||||
}
|
||||
const fscFill = fscFillLine + ` L ${fscSvgW},${fscSvgH} L 0,${fscSvgH} Z`;
|
||||
return html`
|
||||
<svg class="fsc-temp-curve" viewBox="0 0 ${fscSvgW} ${fscSvgH}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="fsc-grad" x1="0" y1="0" x2="1" y2="0" gradientUnits="objectBoundingBox">
|
||||
${fscTemps.map((t, i) => {
|
||||
const fc = utciCategory(t);
|
||||
return html`<stop key=${i} offset=${`${((i + 0.5) / fscN * 100).toFixed(1)}%`} stop-color=${fc.bg} />`;
|
||||
})}
|
||||
</linearGradient>
|
||||
</defs>
|
||||
${fscPts.map((pt, i) => {
|
||||
const isNow = fscNowFlags[i];
|
||||
return html`
|
||||
<line key=${'c'+i} x1=${pt.x} y1="0" x2=${pt.x} y2=${pt.y}
|
||||
stroke=${isNow ? '#c8922a' : '#b09870'}
|
||||
stroke-width="1"
|
||||
opacity=${isNow ? '1' : '0.55'}
|
||||
/>
|
||||
`;
|
||||
})}
|
||||
<path d=${fscFill} fill="url(#fsc-grad)" opacity="0.45" />
|
||||
</svg>
|
||||
`;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=${`utci-table-wrap${tableCanScrollLeft ? ' scroll-fade-left' : ''}${tableCanScrollRight ? ' scroll-fade-right' : ''}${showUnits ? '' : ' no-units'}`} ref=${tableWrapRef}>
|
||||
<span class="utci-scroll-chevron left" aria-hidden="true">‹</span>
|
||||
<span class="utci-scroll-chevron right" aria-hidden="true">›</span>
|
||||
|
||||
@@ -402,7 +402,7 @@ export function WindVane({ bearing, size = 30 }) {
|
||||
// elev: solar elevation in degrees (negative = night)
|
||||
// dt: Date used for moon phase
|
||||
// -------------------------------------------------------------------
|
||||
export function CloudIcon({ category, size = 30, elev = 90, dt = new Date() }) {
|
||||
export function CloudIcon({ category, size = 30, elev = 90, dt = new Date(), filled = false }) {
|
||||
const r = size / 2;
|
||||
const brass = '#c8922a';
|
||||
const ink = '#2a1a08';
|
||||
@@ -464,13 +464,13 @@ export function CloudIcon({ category, size = 30, elev = 90, dt = new Date() }) {
|
||||
${mist(size * 0.18, brass)}
|
||||
${line(size * 0.18, size * 0.56, size * 0.60, size * 0.56, muted, sw * 0.7, 0.6)}`}
|
||||
${category === 'scattered' && html`
|
||||
<path d=${cloudPath(0, -size * 0.2425, 0.82)} fill="none" stroke=${ink}
|
||||
<path d=${cloudPath(0, -size * 0.2425, 0.82)} fill=${filled ? 'rgba(255,255,255,0.88)' : 'none'} stroke=${ink}
|
||||
stroke-width=${sw} stroke-linecap=${cap} stroke-linejoin=${join} />
|
||||
${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`
|
||||
<path d=${cloudPath(-size * 0.02, -size * 0.2275, 0.70)} fill="none" stroke=${muted}
|
||||
<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}
|
||||
stroke-width=${sw * 0.9} stroke-linecap=${cap} stroke-linejoin=${join} opacity="0.82" />
|
||||
<path d=${cloudPath(0, -size * 0.25, 0.88)} fill="none" stroke=${ink}
|
||||
<path d=${cloudPath(0, -size * 0.25, 0.88)} fill=${filled ? 'rgba(255,255,255,0.92)' : 'none'} stroke=${ink}
|
||||
stroke-width=${sw} stroke-linecap=${cap} stroke-linejoin=${join} />`}
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
@@ -343,6 +343,14 @@ export function useAppState() {
|
||||
return next;
|
||||
});
|
||||
|
||||
const [forecastView, setForecastViewState] = useState(() => {
|
||||
try { const s = localStorage.getItem('sunscope_forecast_view'); return s === 'table' ? 'table' : 'simple'; } catch (e) { return 'simple'; }
|
||||
});
|
||||
const setForecastView = (v) => {
|
||||
try { localStorage.setItem('sunscope_forecast_view', v); } catch (e) { /* ignore */ }
|
||||
setForecastViewState(v);
|
||||
};
|
||||
|
||||
const searchTimeout = useRef(null);
|
||||
|
||||
// Derived selectors used by profile controls
|
||||
@@ -603,6 +611,7 @@ export function useAppState() {
|
||||
restoreOpen, openRestore, closeRestore,
|
||||
showUnits, toggleShowUnits,
|
||||
tableInterval, setTableInterval,
|
||||
forecastView, setForecastView,
|
||||
// table refs
|
||||
headStickyRef, headTrackRef, headTableRef,
|
||||
bodyScrollRef, bodyTableRef, tableWrapRef,
|
||||
|
||||
Reference in New Issue
Block a user