3.0 Beta 1

New simple mode display with graph. First render and scaling test.
This commit is contained in:
fraxle
2026-06-12 21:22:18 +01:00
parent f6e6efe747
commit 445fba033d
5 changed files with 341 additions and 8 deletions
+222
View File
@@ -1335,3 +1335,225 @@
letter-spacing: 0.03em;
}
}
/* ── SIMPLE / TABLE VIEW TOGGLE ─────────────────────────────────────────── */
.forecast-view-toggle {
display: inline-flex;
align-items: center;
flex-shrink: 0;
margin-right: 8px;
}
.fvt-btn {
font-family: Manrope, sans-serif;
font-size: 11px;
font-weight: 700;
line-height: 1;
padding: 4px 10px;
cursor: pointer;
border: 1.5px solid #c9b08a;
background: #f5edd6;
color: #6b4228;
transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.fvt-btn:first-child {
border-radius: 3px 0 0 3px;
}
.fvt-btn:last-child {
border-left: none;
border-radius: 0 3px 3px 0;
}
.fvt-btn:hover {
border-color: #c8922a;
background: #fef3dc;
z-index: 1;
}
.fvt-btn.on {
background: #c8922a;
border-color: #c8922a;
color: #fff;
}
/* Interval buttons shown in the col-toggles bar in simple mode */
.fvt-interval {
display: none;
align-items: center;
gap: 2px;
margin-right: 6px;
flex-shrink: 0;
}
.table-main--simple .fvt-interval {
display: inline-flex;
}
/* Hide table-mode column controls when in simple mode */
.table-main--simple .col-toggles-label,
.table-main--simple .col-toggles-edit-btn,
.table-main--simple .col-toggles-body {
display: none !important;
}
/* Show simple view, hide table; swap in table mode */
.forecast-simple-wrap {
display: none;
}
.table-main--simple {
align-self: stretch;
display: flex;
flex-direction: column;
}
.table-main--simple .forecast-simple-wrap {
display: flex;
flex-direction: column;
flex: 1;
border: 1.5px solid #d4c0a0;
background: #fdf8ee;
}
.table-main--simple .utci-table-wrap {
display: none;
}
/* ── SIMPLE VIEW CARDS ───────────────────────────────────────────────────── */
.forecast-simple-scroll {
flex: 1;
display: flex;
flex-direction: column;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: #c9b08a transparent;
}
.forecast-simple-scroll::-webkit-scrollbar {
height: 4px;
}
.forecast-simple-scroll::-webkit-scrollbar-track {
background: transparent;
}
.forecast-simple-scroll::-webkit-scrollbar-thumb {
background: #c9b08a;
border-radius: 2px;
}
.forecast-simple-inner {
flex: 1;
display: flex;
flex-direction: column;
width: max-content;
min-width: 100%;
}
.forecast-simple-cards {
display: grid;
padding: 12px 0 0;
}
.fsc-card {
min-width: 0;
text-align: center;
border: none;
background: transparent;
padding: 10px 8px 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
border-radius: 1px;
}
.fsc-card--now {
border: 1.5px solid #c8922a;
background: #fff9ee;
box-shadow: 0 2px 10px rgba(200,146,42,0.18);
position: relative;
z-index: 1;
}
.fsc-time {
font-family: JetBrains Mono, monospace;
font-size: 12px;
font-weight: 700;
color: #4a3218;
line-height: 1;
}
.fsc-card--now .fsc-time {
color: #c8922a;
}
.fsc-feel {
font-family: Manrope, sans-serif;
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: 2px;
line-height: 1.3;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fsc-scope-wrap {
position: relative;
width: 42px;
height: 42px;
flex-shrink: 0;
}
.fsc-wx-overlay {
position: absolute;
inset: 0;
top: 3px;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.fsc-meta {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
width: 100%;
}
.fsc-meta-row {
font-family: Manrope, sans-serif;
font-size: 10px;
font-weight: 600;
line-height: 1.2;
color: #6b5230;
opacity: 0.8;
}
.fsc-card--now .fsc-meta-row { opacity: 1; color: #4a3218; }
.fsc-meta-rain { color: #2a6a90; }
.fsc-card--now .fsc-meta-rain { color: #1a4a70; }
.fsc-temp {
font-family: Fraunces, serif;
font-size: 22px;
font-weight: 700;
color: #1e1208;
line-height: 1;
}
.fsc-temp-curve {
display: block;
width: 100%;
flex: 1;
min-height: 60px;
}
@media (max-width: 600px) {
.fsc-card {
min-width: 55px;
padding: 8px 4px 10px;
gap: 5px;
}
.fsc-temp {
font-size: 19px;
}
.fsc-feel {
font-size: 9px;
padding: 2px 4px;
}
.fsc-temp-curve {
min-height: 48px;
}
}
+103 -1
View File
@@ -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>
+5 -5
View File
@@ -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>`;
}
+9
View File
@@ -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,
+2 -2
View File
@@ -30,10 +30,10 @@
}
},
"2026-06-12": {
"visits": 27,
"visits": 43,
"profiles": {
"showall": 3,
"basic": 2,
"basic": 5,
"home": 3,
"vehicle": 3,
"alltemps": 2,