From 445fba033d26d5b9780782504191fb1d876eafca Mon Sep 17 00:00:00 2001 From: fraxle Date: Fri, 12 Jun 2026 21:22:18 +0100 Subject: [PATCH] 3.0 Beta 1 New simple mode display with graph. First render and scaling test. --- assets/css/table.css | 222 +++++++++++++++++++++++++++++++++ assets/js/app.js | 104 ++++++++++++++- assets/js/components.js | 10 +- assets/js/hooks/useAppState.js | 9 ++ data/tracking.json | 4 +- 5 files changed, 341 insertions(+), 8 deletions(-) diff --git a/assets/css/table.css b/assets/css/table.css index 8e305f0..8de4870 100644 --- a/assets/css/table.css +++ b/assets/css/table.css @@ -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; + } +} diff --git a/assets/js/app.js b/assets/js/app.js index 2b8e6e3..e792c7b 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -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() {
-
+
+ + + + + + ${[1, 2, 3, 4].map(n => html``)} + Columns:
+
+
+
+
+ ${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` +
+
${localHHMMs}
+
+ <${SkyScope} elev=${r.elev} dt=${r.dt} glob=${r.glob} size=${42} /> + ${domIcon && html`
${domIcon}
`} +
+
+ ${r.precipProb}% + ${windMph}mph + ${r.compass ? r.compass.label : '—'} +
+
${Math.round(r.utciAdj)}°
+
${cat.label}
+
+ `; + })} +
+ ${(() => { + 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` + + + + ${fscTemps.map((t, i) => { + const fc = utciCategory(t); + return html``; + })} + + + ${fscPts.map((pt, i) => { + const isNow = fscNowFlags[i]; + return html` + + `; + })} + + + `; + })()} +
+
+
+
diff --git a/assets/js/components.js b/assets/js/components.js index d4f510f..6222871 100644 --- a/assets/js/components.js +++ b/assets/js/components.js @@ -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` - - ${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` - - `} `; } diff --git a/assets/js/hooks/useAppState.js b/assets/js/hooks/useAppState.js index 62feccb..7806677 100644 --- a/assets/js/hooks/useAppState.js +++ b/assets/js/hooks/useAppState.js @@ -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, diff --git a/data/tracking.json b/data/tracking.json index 61ecea5..12d02bc 100644 --- a/data/tracking.json +++ b/data/tracking.json @@ -30,10 +30,10 @@ } }, "2026-06-12": { - "visits": 27, + "visits": 43, "profiles": { "showall": 3, - "basic": 2, + "basic": 5, "home": 3, "vehicle": 3, "alltemps": 2,