New hour selection
Animated UI elements
This commit is contained in:
fraxle
2026-07-30 23:09:42 +01:00
parent e32a774c1e
commit 2869694634
5 changed files with 165 additions and 40 deletions
+5 -9
View File
@@ -31,7 +31,7 @@ import {
FUR_COLORS,
confidenceBand, moonGlyph, skyFillForElev,
} from './utils.js';
import { SkyScope, WindVane, CloudIcon, ScopeReticle, PrecipIcon, CustomSelect, VentPill } from './components.js';
import { SkyScope, WindVane, CloudIcon, ScopeReticle, PrecipIcon, CustomSelect, VentPill, RowStepper } from './components.js';
import { getCellTagEvents, getUpcomingEvents } from './events.js';
import { POLLEN_TYPES, COL_DESCRIPTIONS, UTCI_ENVIRONMENTS, FILTER_PROFILES, variantIcons, deriveProfileMain } from './config.js';
import { useAppState } from './hooks/useAppState.js';
@@ -1103,10 +1103,10 @@ export function UTCIForecast() {
`;
})()}
</div>
<div key=${forecastView} class=${'col-toggles-wrap' + (colTogglesOpen ? ' col-toggles-wrap--open' : '')}>
<div class=${'col-toggles' + (colTogglesOpen ? ' col-toggles--open' : '')}>
<span class="fvt-interval">
<span class="fvt-interval-label">Hours:</span>
${[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>`)}
<${RowStepper} value=${tableInterval} onChange=${setTableInterval} />
</span>
<div class="col-toggles-body">
${isPro && html`<${Fragment}>
@@ -1157,6 +1157,7 @@ export function UTCIForecast() {
</span>
</div>
</div>
</div>
<div class="forecast-simple-wrap">
<div class="forecast-simple-scroll" ref=${fscScrollRef}>
@@ -1291,12 +1292,7 @@ export function UTCIForecast() {
<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('hour', e)} onMouseEnter=${(e) => handleThEnter('hour', e)} onMouseLeave=${handleThLeave}>
<span>Hour</span>
<span class="hour-interval" onClick=${(e) => e.stopPropagation()}>
${[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=${(e) => { e.stopPropagation(); setTableInterval(n); }}
>${n}h</button>`)}
<${RowStepper} value=${tableInterval} onChange=${setTableInterval} showLabel=${false} />
</span>
</th>
${visibleCols.utciP && html`<th class=${`col-info-th ${groupStart('utciP')} ${groupColor('utciP')}`} scope="col" onClick=${(e) => handleThClick('utciP', e)} onMouseEnter=${(e) => handleThEnter('utciP', e)} onMouseLeave=${handleThLeave}>SunSoak <span class="col-unit">°C felt</span>${UTCI_ENVIRONMENTS[utciEnv]?.shortLabel ? html`<span class="col-env-badge">${UTCI_ENVIRONMENTS[utciEnv].shortLabel}</span>` : null}</th>`}
+35
View File
@@ -7,6 +7,7 @@
// Exports (in order of appearance):
// CustomSelect({ value, options, onChange, ... }) styled dropdown pill
// VentPill({ checked, onChange, label, title }) checkbox fused to pill
// RowStepper({ value, onChange, showLabel }) table row-interval stepper
// SkyScope({ elev, dt, glob, size }) small porthole per hour
// WindVane({ bearing, size }) compass-arrow widget
// CloudIcon({ category, size, elev, dt }) cloud/sun glyph
@@ -257,6 +258,40 @@ export function VentPill({ checked, onChange, label, title, grpClass }) {
</label>`;
}
// -------------------------------------------------------------------
// ROWSTEPPER - "Rows [-] 3h [+]"
// -------------------------------------------------------------------
// Sets how many hours each forecast row covers. NOT a lookahead: the
// value picks the clock-aligned bucket size used by aggregateRows()
// (1h = every hour, 4h = quarter-day blocks).
//
// "-" steps toward 1h (finer, more rows), "+" toward 4h (coarser).
// Direction follows the readout, so + always raises the number shown.
//
// props:
// value - 1..4
// onChange - fn(n)
// showLabel - draw the "Rows" caption (off inside the Hour header,
// where the column title already supplies the context)
// min/max - bounds, so 6h/12h can be added without a redesign
// -------------------------------------------------------------------
export function RowStepper({ value, onChange, showLabel = true, min = 1, max = 4 }) {
const v = Math.min(max, Math.max(min, Number(value) || min));
const step = (n) => { const next = Math.min(max, Math.max(min, n)); if (next !== v) onChange(next); };
return html`
<span class="row-stepper" role="group" aria-label="Row interval">
${showLabel && html`<span class="row-stepper-label">Rows</span>`}
<button type="button" class="row-stepper-btn" disabled=${v <= min}
title="Finer rows" aria-label="Finer rows - fewer hours per row"
onClick=${() => step(v - 1)}></button>
<span class="row-stepper-value" aria-live="polite"
title=${v === 1 ? 'Every hour' : `Every ${v} hours`}>${v}h</span>
<button type="button" class="row-stepper-btn" disabled=${v >= max}
title="Coarser rows" aria-label="Coarser rows - more hours per row"
onClick=${() => step(v + 1)}>+</button>
</span>`;
}
// -------------------------------------------------------------------
// SKYSCOPE - the little porthole circle next to each hour.
// -------------------------------------------------------------------
+1 -1
View File
@@ -251,7 +251,7 @@ export const workVariantKeys = ['farming', 'construction', 'market', 'windowclea
// Tooltip text shown when the user clicks/hovers a table column header.
export const COL_DESCRIPTIONS = {
hour: { title: 'Hour', desc: 'Local wall-clock time for this forecast row. Each row covers one hour.' },
hour: { title: 'Hour', desc: 'Local wall-clock time for this forecast row. Each row covers the interval set by the Rows stepper, clock-aligned from midnight — at 3h, a row labelled 09:00 spans 09:0011:59. Totals like rainfall are summed across the block, risk figures show the block\'s peak, and smooth readings like air temperature are averaged.' },
air: { title: 'Air Temperature', desc: 'Measured air temperature at 2 m above the ground. This is the standard thermometer reading — it does not account for sun, wind, or humidity.' },
shadeT: { title: 'Shade Air', desc: 'Estimated air temperature sitting in the typical shade of the selected Solar Model — building shadow, beach umbrella, tree canopy, boat panels, and so on. Sheltered, sun-warmed surroundings push it above the official open-ground Air figure on calm sunny hours; wind and cloud pull it back toward it. This is an estimate, not a measurement, and differs from the standardised 2 m Air reading.' },
rh: { title: 'Relative Humidity', desc: 'How much moisture the air holds relative to its maximum capacity at that temperature. High RH makes warm days feel stickier and cold days feel rawer.' },
+3 -3
View File
@@ -529,12 +529,12 @@ export function useAppState() {
return next;
});
// Table row interval: 1 = every hour (default), 2/3/4 = clock-aligned buckets.
// Table row interval: 1 = every hour, 2/3/4 = clock-aligned buckets. Default 2.
const [tableInterval, setTableIntervalState] = useState(() => {
try { const n = parseInt(localStorage.getItem('sunscope_table_interval'), 10); return [1, 2, 3, 4].includes(n) ? n : 3; } catch (e) { return 3; }
try { const n = parseInt(localStorage.getItem('sunscope_table_interval'), 10); return [1, 2, 3, 4].includes(n) ? n : 2; } catch (e) { return 2; }
});
const setTableInterval = (n) => setTableIntervalState(() => {
const next = [1, 2, 3, 4].includes(n) ? n : 1;
const next = [1, 2, 3, 4].includes(n) ? n : 2;
try { localStorage.setItem('sunscope_table_interval', String(next)); } catch (e) { /* ignore */ }
return next;
});