1.43 - Extra Pulldown & New Cloud Icons
This commit is contained in:
+152
-62
@@ -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) }}>
|
||||
|
||||
Reference in New Issue
Block a user