1.43 - Extra Pulldown & New Cloud Icons

This commit is contained in:
fraxle
2026-05-16 23:34:57 +01:00
parent 40f651eb98
commit 048e52a177
8 changed files with 305 additions and 221 deletions
+152 -62
View File
@@ -223,6 +223,60 @@ export function UTCIForecast() {
const [indoorMode, setIndoorMode] = useState('off');
const searchTimeout = useRef(null);
const activateProfile = (key) => {
const profile = FILTER_PROFILES[key];
setActiveProfile(key);
if (key !== 'custom') {
setVisibleCols({ ...profile.cols });
const hasIndoor = profile.cols['indoorT'] || profile.cols['managedT'];
setIndoorMode(hasIndoor ? 'on' : 'off');
setIndoorManaged(false);
}
};
const profileButtonOrder = ['basic', 'home', 'vehicle', 'alltemps', 'custom'];
const variantIcons = {
urban: '🏙️',
beach: '🏖️',
events: '🎪',
festival: '⛺',
sailing: '⛵',
wintersports: '🎿',
naturist: '☀️',
};
const activityVariantKeys = ['sailing', 'wintersports', 'naturist'];
const placeVariantKeys = ['urban', 'beach', 'events', 'festival'];
const activityOptions = [
{
value: 'farming',
label: `${FILTER_PROFILES.farming.icon} ${FILTER_PROFILES.farming.label}`,
},
...activityVariantKeys.map((k) => {
const v = OUTDOORS_VARIANTS[k];
return {
value: k,
label: `${variantIcons[k]} ${v.name}${v.proOnly && !isPro ? ' 🔒' : ''}`,
};
}),
];
const placeOptions = placeVariantKeys.map((k) => {
const v = OUTDOORS_VARIANTS[k];
return {
value: k,
label: `${variantIcons[k]} ${v.name}${v.proOnly && !isPro ? ' 🔒' : ''}`,
};
});
const activityValue = activeProfile === 'farming'
? 'farming'
: activeProfile === 'outdoors' && activityVariantKeys.includes(outdoorsVariant)
? outdoorsVariant
: 'off';
const activityLabel = activityOptions.find((option) => option.value === activityValue)?.label;
const placeValue = activeProfile === 'outdoors' && placeVariantKeys.includes(outdoorsVariant)
? outdoorsVariant
: 'off';
const placeLabel = placeOptions.find((option) => option.value === placeValue)?.label;
// Refs for the two-scroller table layout (sticky-to-viewport header +
// horizontally-scrolling body). The header is clipped (overflow:hidden)
// and its inner "track" gets translateX'd via JS to follow the body's
@@ -874,7 +928,7 @@ export function UTCIForecast() {
<div class="event-banner-title">${ev.title}</div>
<div class="event-banner-msg">${ev.message}</div>
${dateLine && html`
<div class="event-banner-dates" style=${{ opacity: 0.75, fontSize: '11px', fontFamily: 'JetBrains Mono, monospace', letterSpacing: '0.08em', textTransform: 'uppercase', marginTop: '5px' }}>
<div class="event-banner-dates" style=${{ opacity: 0.75, fontSize: '11px', fontFamily: 'Manrope, sans-serif', fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase', marginTop: '5px' }}>
${dateLine}
</div>
`}
@@ -1072,7 +1126,7 @@ export function UTCIForecast() {
const dayName = promptDate.toLocaleDateString('en-GB', { weekday: 'long', timeZone: 'UTC' });
const dayLong = promptDate.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'short', timeZone: 'UTC' });
const extraPromptCopy = {
'profile:sailing': {
'variant:sailing': {
title: 'Sailing is part of SunScope Extra',
detail: 'Extra adds specialist planning views for higher-commitment trips, including wind, exposure, UV, and wet-weather comfort for water conditions.',
},
@@ -1136,10 +1190,11 @@ export function UTCIForecast() {
${promptCopy.detail}
</div>
<div style=${{
fontFamily: 'JetBrains Mono, monospace',
fontFamily: 'Manrope, sans-serif',
fontWeight: 700,
fontSize: '11px',
textTransform: 'uppercase',
letterSpacing: '0.14em',
letterSpacing: '0.07em',
color: '#c8922a',
marginTop: '10px',
}}>
@@ -1162,10 +1217,11 @@ export function UTCIForecast() {
background: '#c8922a',
color: '#fff',
textDecoration: 'none',
fontFamily: 'JetBrains Mono, monospace',
fontFamily: 'Manrope, sans-serif',
fontWeight: 700,
fontSize: '11px',
textTransform: 'uppercase',
letterSpacing: '0.14em',
letterSpacing: '0.07em',
borderRadius: '3px',
whiteSpace: 'nowrap',
}}
@@ -1175,10 +1231,11 @@ export function UTCIForecast() {
<a
href="/restore.php"
style=${{
fontFamily: 'JetBrains Mono, monospace',
fontFamily: 'Manrope, sans-serif',
fontWeight: 700,
fontSize: '10px',
textTransform: 'uppercase',
letterSpacing: '0.14em',
letterSpacing: '0.07em',
color: '#c8922a',
textDecoration: 'none',
borderBottom: '1px solid rgba(200,146,42,0.4)',
@@ -1192,10 +1249,11 @@ export function UTCIForecast() {
target="_blank"
rel="noopener noreferrer"
style=${{
fontFamily: 'JetBrains Mono, monospace',
fontFamily: 'Manrope, sans-serif',
fontWeight: 700,
fontSize: '10px',
textTransform: 'uppercase',
letterSpacing: '0.14em',
letterSpacing: '0.07em',
color: '#b09870',
textDecoration: 'none',
borderBottom: '1px solid rgba(176,152,112,0.4)',
@@ -1210,10 +1268,11 @@ export function UTCIForecast() {
background: 'transparent',
border: 'none',
cursor: 'pointer',
fontFamily: 'JetBrains Mono, monospace',
fontFamily: 'Manrope, sans-serif',
fontWeight: 700,
fontSize: '10px',
textTransform: 'uppercase',
letterSpacing: '0.14em',
letterSpacing: '0.07em',
color: '#b09870',
padding: '2px 4px',
}}
@@ -1239,10 +1298,11 @@ export function UTCIForecast() {
borderRadius: '0 4px 4px 0',
fontSize: '12px',
color: '#3a2a18',
fontFamily: 'JetBrains Mono, monospace',
fontFamily: 'Manrope, sans-serif',
fontWeight: 600,
flexWrap: 'wrap',
}}>
<span style=${{ letterSpacing: '1px', textTransform: 'uppercase', fontWeight: 700 }}>
<span style=${{ letterSpacing: '0.06em', textTransform: 'uppercase', fontWeight: 700 }}>
Day ${selectedDay + 1} of 14 · Forecast clarity: ${band.label}
</span>
${isOutlook && html`
@@ -1261,45 +1321,10 @@ export function UTCIForecast() {
-->
<div class="filter-profiles" style=${{ borderBottom: isPro ? 'none' : '' }}>
<span class="filter-profiles-label">Profile:</span>
${Object.entries(FILTER_PROFILES).map(([key, profile]) => {
${profileButtonOrder.slice(0, 3).map((key) => {
const profile = FILTER_PROFILES[key];
const locked = profile.proOnly && !isPro;
// The 'outdoors' profile becomes a CustomSelect dropdown,
// followed by a separator dividing general-use from technical profiles.
if (key === 'outdoors') {
return html`
<${CustomSelect}
key="outdoors"
value=${activeProfile === 'outdoors' ? outdoorsVariant : 'off'}
isOn=${activeProfile === 'outdoors'}
noHide=${true}
hideLabel=${`${profile.icon} ${profile.label}`}
options=${Object.entries(OUTDOORS_VARIANTS).map(([k, v]) => ({
value: k,
label: v.name + (v.proOnly && !isPro ? ' 🔒' : ''),
disabled: false,
}))}
onChange=${(v) => {
if (v === 'off') {
setActiveProfile('basic');
setVisibleCols({ ...FILTER_PROFILES.basic.cols });
setIndoorMode('off');
return;
}
if (OUTDOORS_VARIANTS[v]?.proOnly && !isPro) {
setProPromptSource(`variant:${v}`);
setProPromptDay(0);
return;
}
setActiveProfile('outdoors');
setOutdoorsVariant(v);
setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols });
setIndoorMode('off');
}}
/>
<span class="profile-divider" aria-hidden="true"></span>`;
}
return html`
<button
key=${key}
@@ -1312,17 +1337,82 @@ export function UTCIForecast() {
setProPromptDay(0);
return;
}
setActiveProfile(key);
if (key !== 'custom') {
setVisibleCols({ ...profile.cols });
}
// Auto-enable indoor column for profiles that include it;
// reset to off for profiles that don't.
if (key !== 'custom') {
const hasIndoor = profile.cols['indoorT'] || profile.cols['managedT'];
setIndoorMode(hasIndoor ? 'on' : 'off');
setIndoorManaged(false);
activateProfile(key);
}}
>${profile.icon} ${profile.label}${locked ? ' 🔒' : ''}</button>`;
})}
<${CustomSelect}
key="activities"
value=${activityValue}
isOn=${activeProfile === 'farming' || (activeProfile === 'outdoors' && activityVariantKeys.includes(outdoorsVariant))}
noHide=${true}
hideLabel="🎯 Activities"
buttonLabel=${activityValue === 'off' ? '🎯 Activities' : `🎯 Activities: ${activityLabel}`}
options=${activityOptions}
onChange=${(v) => {
if (v === 'off') {
activateProfile('basic');
return;
}
if (v === 'farming') {
activateProfile('farming');
return;
}
if (OUTDOORS_VARIANTS[v]?.proOnly && !isPro) {
setProPromptSource(`variant:${v}`);
setProPromptDay(0);
return;
}
setActiveProfile('outdoors');
setOutdoorsVariant(v);
setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols });
setIndoorMode('off');
setIndoorManaged(false);
}}
/>
<${CustomSelect}
key="places"
value=${placeValue}
isOn=${activeProfile === 'outdoors' && placeVariantKeys.includes(outdoorsVariant)}
noHide=${true}
hideLabel="🌤️ Places"
buttonLabel=${placeValue === 'off' ? '🌤️ Places' : `🌤️ Places: ${placeLabel}`}
options=${placeOptions}
onChange=${(v) => {
if (v === 'off') {
activateProfile('basic');
return;
}
if (OUTDOORS_VARIANTS[v]?.proOnly && !isPro) {
setProPromptSource(`variant:${v}`);
setProPromptDay(0);
return;
}
setActiveProfile('outdoors');
setOutdoorsVariant(v);
setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols });
setIndoorMode('off');
setIndoorManaged(false);
}}
/>
<span class="profile-divider" aria-hidden="true"></span>
${profileButtonOrder.slice(3).map((key) => {
const profile = FILTER_PROFILES[key];
const locked = profile.proOnly && !isPro;
return html`
<button
key=${key}
class=${`profile-btn${activeProfile === key ? ' active' : ''}${locked ? ' locked' : ''}`}
style=${{ opacity: locked ? 0.6 : 1, cursor: locked ? 'not-allowed' : 'pointer', position: 'relative' }}
title=${locked ? `🔒 ${profile.label} is part of SunScope Extra` : ''}
onClick=${() => {
if (locked) {
setProPromptSource(`profile:${key}`);
setProPromptDay(0);
return;
}
activateProfile(key);
}}
>${profile.icon} ${profile.label}${locked ? ' 🔒' : ''}</button>`;
})}
@@ -1601,7 +1691,7 @@ ${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['a
${visibleCols.dir && html`<td class="utci-dir-cell">
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '6px', verticalAlign: 'middle' }}>
<${WindVane} bearing=${r.wd} size=${28} />
<span class="wind-dir-label" style=${{ fontFamily: 'JetBrains Mono, monospace', fontSize: '11px' }}>${r.compass.label}</span>
<span class="wind-dir-label" style=${{ fontFamily: 'Manrope, sans-serif', fontSize: '11px', fontWeight: 700 }}>${r.compass.label}</span>
</span>
</td>`}
${visibleCols.cloud && html`<td style=${{ background: scaleBg(r.cc, 0, 100, '110,130,150', 0.07) }}>