1.43 - Extra Pulldown & New Cloud Icons
This commit is contained in:
+3
-2
@@ -23,9 +23,10 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header a {
|
header a {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'Manrope', sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
letter-spacing: 0.10em;
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: #b09870;
|
color: #b09870;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
+5
-4
@@ -27,8 +27,8 @@
|
|||||||
FONTS
|
FONTS
|
||||||
──────────────────────────────────────────────────────────────────────
|
──────────────────────────────────────────────────────────────────────
|
||||||
Fraunces serif display titles, day labels, hour cell
|
Fraunces serif display titles, day labels, hour cell
|
||||||
Manrope body sans-serif paragraphs
|
Manrope body sans-serif paragraphs, small labels
|
||||||
JetBrains Mono numeric monospace table data, small labels
|
JetBrains Mono numeric monospace table data
|
||||||
════════════════════════════════════════════════════════════════════════ */
|
════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
|
||||||
@@ -92,9 +92,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.utci-topnav a {
|
.utci-topnav a {
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
letter-spacing: 0.1em;
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: #b09870;
|
color: #b09870;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -65,10 +65,11 @@
|
|||||||
|
|
||||||
/* The credits line under the tagline */
|
/* The credits line under the tagline */
|
||||||
.utci-subtitle {
|
.utci-subtitle {
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.18em;
|
letter-spacing: 0.08em;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
color: #b09870;
|
color: #b09870;
|
||||||
}
|
}
|
||||||
@@ -121,10 +122,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.utci-search-label {
|
.utci-search-label {
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.18em;
|
letter-spacing: 0.08em;
|
||||||
color: #b09870;
|
color: #b09870;
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
@@ -193,8 +195,9 @@
|
|||||||
margin-top: 7px;
|
margin-top: 7px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #b09870;
|
color: #b09870;
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
letter-spacing: 0.14em;
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-18
@@ -88,10 +88,11 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.14em;
|
letter-spacing: 0.08em;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: #b09870;
|
color: #b09870;
|
||||||
@@ -137,10 +138,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filter-profiles-label {
|
.filter-profiles-label {
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.18em;
|
letter-spacing: 0.08em;
|
||||||
color: #7a5c2a;
|
color: #7a5c2a;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -162,10 +164,11 @@
|
|||||||
border: 1.5px solid #c9b08a;
|
border: 1.5px solid #c9b08a;
|
||||||
background: #f5edd6;
|
background: #f5edd6;
|
||||||
color: #6b4228;
|
color: #6b4228;
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.06em;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -198,10 +201,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.col-toggles-label {
|
.col-toggles-label {
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.18em;
|
letter-spacing: 0.08em;
|
||||||
color: #7a5c2a;
|
color: #7a5c2a;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -214,10 +218,11 @@
|
|||||||
border: 1.5px solid #c9b08a;
|
border: 1.5px solid #c9b08a;
|
||||||
background: #f5edd6;
|
background: #f5edd6;
|
||||||
color: #6b4228;
|
color: #6b4228;
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.06em;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -297,8 +302,9 @@
|
|||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 5px 12px 5px 10px;
|
padding: 5px 12px 5px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
letter-spacing: 0.08em;
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1.5px solid #c9b08a;
|
border: 1.5px solid #c9b08a;
|
||||||
@@ -465,11 +471,11 @@
|
|||||||
.utci-table th {
|
.utci-table th {
|
||||||
background: #f0e4c4;
|
background: #f0e4c4;
|
||||||
color: #6b4f2a;
|
color: #6b4f2a;
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.07em;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
padding: 14px 12px;
|
padding: 14px 12px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -478,7 +484,7 @@
|
|||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
.utci-table th {
|
.utci-table th {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
letter-spacing: 0.14em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,8 +709,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.utci-band-label {
|
.utci-band-label {
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-21
@@ -128,10 +128,11 @@
|
|||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
.almanac-subtitle {
|
.almanac-subtitle {
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.07em;
|
||||||
color: #9a7d5a;
|
color: #9a7d5a;
|
||||||
}
|
}
|
||||||
.almanac-list {
|
.almanac-list {
|
||||||
@@ -177,16 +178,18 @@
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
.almanac-entry-date {
|
.almanac-entry-date {
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.06em;
|
||||||
color: #c8922a;
|
color: #c8922a;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.almanac-entry-countdown {
|
.almanac-entry-countdown {
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
color: #9a7d5a;
|
color: #9a7d5a;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -200,10 +203,11 @@
|
|||||||
.almanac-entry-visibility {
|
.almanac-entry-visibility {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.06em;
|
||||||
color: #7a9a6a;
|
color: #7a9a6a;
|
||||||
background: #eef4e8;
|
background: #eef4e8;
|
||||||
border: 1px solid #c0d8b0;
|
border: 1px solid #c0d8b0;
|
||||||
@@ -250,10 +254,11 @@
|
|||||||
border: 1.5px solid #c9b08a;
|
border: 1.5px solid #c9b08a;
|
||||||
background: #f5edd6;
|
background: #f5edd6;
|
||||||
color: #6b4228;
|
color: #6b4228;
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.06em;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -328,10 +333,11 @@
|
|||||||
.cs-option {
|
.cs-option {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.05em;
|
||||||
color: #4a3420;
|
color: #4a3420;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.1s, color 0.1s;
|
transition: background 0.1s, color 0.1s;
|
||||||
@@ -369,8 +375,9 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 5px 12px 5px 10px;
|
padding: 5px 12px 5px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
letter-spacing: 0.08em;
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1.5px solid #c9b08a;
|
border: 1.5px solid #c9b08a;
|
||||||
@@ -430,10 +437,11 @@
|
|||||||
padding: 14px 18px;
|
padding: 14px 18px;
|
||||||
background: #f0e4c4;
|
background: #f0e4c4;
|
||||||
color: #7a5c2a;
|
color: #7a5c2a;
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.07em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
border: 1.5px solid #c9b08a;
|
border: 1.5px solid #c9b08a;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
@@ -450,10 +458,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.utci-legend-label {
|
.utci-legend-label {
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.18em;
|
letter-spacing: 0.08em;
|
||||||
color: #7a5c2a;
|
color: #7a5c2a;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -468,10 +476,11 @@
|
|||||||
|
|
||||||
.utci-legend-item {
|
.utci-legend-item {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.04em;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 110px;
|
min-width: 110px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -536,10 +545,11 @@
|
|||||||
margin-top: 36px;
|
margin-top: 36px;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
border-top: 1px solid #d4c0a0;
|
border-top: 1px solid #d4c0a0;
|
||||||
font-family: JetBrains Mono, monospace;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.07em;
|
||||||
color: #9a7d5a;
|
color: #9a7d5a;
|
||||||
line-height: 2.2;
|
line-height: 2.2;
|
||||||
}
|
}
|
||||||
|
|||||||
+152
-62
@@ -223,6 +223,60 @@ export function UTCIForecast() {
|
|||||||
const [indoorMode, setIndoorMode] = useState('off');
|
const [indoorMode, setIndoorMode] = useState('off');
|
||||||
const searchTimeout = useRef(null);
|
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 +
|
// Refs for the two-scroller table layout (sticky-to-viewport header +
|
||||||
// horizontally-scrolling body). The header is clipped (overflow:hidden)
|
// horizontally-scrolling body). The header is clipped (overflow:hidden)
|
||||||
// and its inner "track" gets translateX'd via JS to follow the body's
|
// 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-title">${ev.title}</div>
|
||||||
<div class="event-banner-msg">${ev.message}</div>
|
<div class="event-banner-msg">${ev.message}</div>
|
||||||
${dateLine && html`
|
${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}
|
${dateLine}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
@@ -1072,7 +1126,7 @@ export function UTCIForecast() {
|
|||||||
const dayName = promptDate.toLocaleDateString('en-GB', { weekday: 'long', timeZone: 'UTC' });
|
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 dayLong = promptDate.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'short', timeZone: 'UTC' });
|
||||||
const extraPromptCopy = {
|
const extraPromptCopy = {
|
||||||
'profile:sailing': {
|
'variant:sailing': {
|
||||||
title: 'Sailing is part of SunScope Extra',
|
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.',
|
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}
|
${promptCopy.detail}
|
||||||
</div>
|
</div>
|
||||||
<div style=${{
|
<div style=${{
|
||||||
fontFamily: 'JetBrains Mono, monospace',
|
fontFamily: 'Manrope, sans-serif',
|
||||||
|
fontWeight: 700,
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: '0.14em',
|
letterSpacing: '0.07em',
|
||||||
color: '#c8922a',
|
color: '#c8922a',
|
||||||
marginTop: '10px',
|
marginTop: '10px',
|
||||||
}}>
|
}}>
|
||||||
@@ -1162,10 +1217,11 @@ export function UTCIForecast() {
|
|||||||
background: '#c8922a',
|
background: '#c8922a',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
fontFamily: 'JetBrains Mono, monospace',
|
fontFamily: 'Manrope, sans-serif',
|
||||||
|
fontWeight: 700,
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: '0.14em',
|
letterSpacing: '0.07em',
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
@@ -1175,10 +1231,11 @@ export function UTCIForecast() {
|
|||||||
<a
|
<a
|
||||||
href="/restore.php"
|
href="/restore.php"
|
||||||
style=${{
|
style=${{
|
||||||
fontFamily: 'JetBrains Mono, monospace',
|
fontFamily: 'Manrope, sans-serif',
|
||||||
|
fontWeight: 700,
|
||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: '0.14em',
|
letterSpacing: '0.07em',
|
||||||
color: '#c8922a',
|
color: '#c8922a',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
borderBottom: '1px solid rgba(200,146,42,0.4)',
|
borderBottom: '1px solid rgba(200,146,42,0.4)',
|
||||||
@@ -1192,10 +1249,11 @@ export function UTCIForecast() {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
style=${{
|
style=${{
|
||||||
fontFamily: 'JetBrains Mono, monospace',
|
fontFamily: 'Manrope, sans-serif',
|
||||||
|
fontWeight: 700,
|
||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: '0.14em',
|
letterSpacing: '0.07em',
|
||||||
color: '#b09870',
|
color: '#b09870',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
borderBottom: '1px solid rgba(176,152,112,0.4)',
|
borderBottom: '1px solid rgba(176,152,112,0.4)',
|
||||||
@@ -1210,10 +1268,11 @@ export function UTCIForecast() {
|
|||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontFamily: 'JetBrains Mono, monospace',
|
fontFamily: 'Manrope, sans-serif',
|
||||||
|
fontWeight: 700,
|
||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: '0.14em',
|
letterSpacing: '0.07em',
|
||||||
color: '#b09870',
|
color: '#b09870',
|
||||||
padding: '2px 4px',
|
padding: '2px 4px',
|
||||||
}}
|
}}
|
||||||
@@ -1239,10 +1298,11 @@ export function UTCIForecast() {
|
|||||||
borderRadius: '0 4px 4px 0',
|
borderRadius: '0 4px 4px 0',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: '#3a2a18',
|
color: '#3a2a18',
|
||||||
fontFamily: 'JetBrains Mono, monospace',
|
fontFamily: 'Manrope, sans-serif',
|
||||||
|
fontWeight: 600,
|
||||||
flexWrap: 'wrap',
|
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}
|
Day ${selectedDay + 1} of 14 · Forecast clarity: ${band.label}
|
||||||
</span>
|
</span>
|
||||||
${isOutlook && html`
|
${isOutlook && html`
|
||||||
@@ -1261,45 +1321,10 @@ export function UTCIForecast() {
|
|||||||
-->
|
-->
|
||||||
<div class="filter-profiles" style=${{ borderBottom: isPro ? 'none' : '' }}>
|
<div class="filter-profiles" style=${{ borderBottom: isPro ? 'none' : '' }}>
|
||||||
<span class="filter-profiles-label">Profile:</span>
|
<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;
|
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`
|
return html`
|
||||||
<button
|
<button
|
||||||
key=${key}
|
key=${key}
|
||||||
@@ -1312,17 +1337,82 @@ export function UTCIForecast() {
|
|||||||
setProPromptDay(0);
|
setProPromptDay(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setActiveProfile(key);
|
activateProfile(key);
|
||||||
if (key !== 'custom') {
|
}}
|
||||||
setVisibleCols({ ...profile.cols });
|
>${profile.icon} ${profile.label}${locked ? ' 🔒' : ''}</button>`;
|
||||||
}
|
})}
|
||||||
// Auto-enable indoor column for profiles that include it;
|
<${CustomSelect}
|
||||||
// reset to off for profiles that don't.
|
key="activities"
|
||||||
if (key !== 'custom') {
|
value=${activityValue}
|
||||||
const hasIndoor = profile.cols['indoorT'] || profile.cols['managedT'];
|
isOn=${activeProfile === 'farming' || (activeProfile === 'outdoors' && activityVariantKeys.includes(outdoorsVariant))}
|
||||||
setIndoorMode(hasIndoor ? 'on' : 'off');
|
noHide=${true}
|
||||||
setIndoorManaged(false);
|
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>`;
|
>${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">
|
${visibleCols.dir && html`<td class="utci-dir-cell">
|
||||||
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '6px', verticalAlign: 'middle' }}>
|
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '6px', verticalAlign: 'middle' }}>
|
||||||
<${WindVane} bearing=${r.wd} size=${28} />
|
<${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>
|
</span>
|
||||||
</td>`}
|
</td>`}
|
||||||
${visibleCols.cloud && html`<td style=${{ background: scaleBg(r.cc, 0, 100, '110,130,150', 0.07) }}>
|
${visibleCols.cloud && html`<td style=${{ background: scaleBg(r.cc, 0, 100, '110,130,150', 0.07) }}>
|
||||||
|
|||||||
+74
-104
@@ -37,7 +37,7 @@ const html = htm.bind(h);
|
|||||||
// groupedLeft — bool — fuse right side with a VentPill
|
// groupedLeft — bool — fuse right side with a VentPill
|
||||||
// isLastChild — bool — restore right border-radius when no sibling follows
|
// isLastChild — bool — restore right border-radius when no sibling follows
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
export function CustomSelect({ value, options, onChange, hideLabel, hidingLabel, isOn, noHide, groupedLeft, isLastChild }) {
|
export function CustomSelect({ value, options, onChange, hideLabel, hidingLabel, buttonLabel, isOn, noHide, groupedLeft, isLastChild }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const wrapRef = useRef(null);
|
const wrapRef = useRef(null);
|
||||||
|
|
||||||
@@ -60,9 +60,9 @@ export function CustomSelect({ value, options, onChange, hideLabel, hidingLabel,
|
|||||||
|
|
||||||
// Build the label shown on the button
|
// Build the label shown on the button
|
||||||
const activeOpt = options.find(o => o.value === value);
|
const activeOpt = options.find(o => o.value === value);
|
||||||
const btnLabel = isOn
|
const btnLabel = buttonLabel || (isOn
|
||||||
? (activeOpt ? activeOpt.label : hideLabel)
|
? (activeOpt ? activeOpt.label : hideLabel)
|
||||||
: hideLabel;
|
: hideLabel);
|
||||||
|
|
||||||
const btnClass = [
|
const btnClass = [
|
||||||
'cs-btn',
|
'cs-btn',
|
||||||
@@ -157,8 +157,6 @@ export function SkyScope({ elev, dt, glob = 0, size = 26 }) {
|
|||||||
const isRising = localHour < 12;
|
const isRising = localHour < 12;
|
||||||
|
|
||||||
const skyGrad = skyGradientForElev(elev, isRising);
|
const skyGrad = skyGradientForElev(elev, isRising);
|
||||||
// Flat horizon colour used for moon shadow fill
|
|
||||||
const skyHorizon = skyGrad.bot;
|
|
||||||
|
|
||||||
const isDay = elev > 0; // sun only drawn when actually above horizon
|
const isDay = elev > 0; // sun only drawn when actually above horizon
|
||||||
const isTwil = elev > -8 && !isDay; // moon shown in twilight too
|
const isTwil = elev > -8 && !isDay; // moon shown in twilight too
|
||||||
@@ -173,7 +171,6 @@ export function SkyScope({ elev, dt, glob = 0, size = 26 }) {
|
|||||||
// so it's intense at the sun centre and fades smoothly to transparent.
|
// so it's intense at the sun centre and fades smoothly to transparent.
|
||||||
const radFrac = Math.min(1, (glob || 0) / 900); // 0 (dark) → 1 (blazing)
|
const radFrac = Math.min(1, (glob || 0) / 900); // 0 (dark) → 1 (blazing)
|
||||||
const horizFrac = Math.max(0, 1 - elev / 20); // extra warmth near horizon
|
const horizFrac = Math.max(0, 1 - elev / 20); // extra warmth near horizon
|
||||||
const glowColor = isRising ? '#ffb040' : '#ffc060';
|
|
||||||
const glowR = sunR + 4 + radFrac * 6; // total glow radius
|
const glowR = sunR + 4 + radFrac * 6; // total glow radius
|
||||||
const glowPeak = 0.35 + radFrac * 0.45 + horizFrac * 0.15; // centre opacity
|
const glowPeak = 0.35 + radFrac * 0.45 + horizFrac * 0.15; // centre opacity
|
||||||
|
|
||||||
@@ -291,7 +288,7 @@ export function WindVane({ bearing, size = 30 }) {
|
|||||||
style=${{ flexShrink: 0, display: 'inline-block', verticalAlign: 'middle', overflow: 'visible' }}>
|
style=${{ flexShrink: 0, display: 'inline-block', verticalAlign: 'middle', overflow: 'visible' }}>
|
||||||
<!-- Static N marker — small letter above the centre, doesn't rotate -->
|
<!-- Static N marker — small letter above the centre, doesn't rotate -->
|
||||||
<text x=${r} y=${size * 0.18} text-anchor="middle"
|
<text x=${r} y=${size * 0.18} text-anchor="middle"
|
||||||
font-family="JetBrains Mono, monospace"
|
font-family="Manrope, sans-serif"
|
||||||
font-size=${size * 0.26} font-weight="700"
|
font-size=${size * 0.26} font-weight="700"
|
||||||
fill=${nColor} opacity="0.55">N</text>
|
fill=${nColor} opacity="0.55">N</text>
|
||||||
<!-- Rotating arrow. Shaft + ONE arrowhead. A small round nub on
|
<!-- Rotating arrow. Shaft + ONE arrowhead. A small round nub on
|
||||||
@@ -312,9 +309,8 @@ export function WindVane({ bearing, size = 30 }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
// CLOUDICON — clouds + sun/moon on a fully transparent background.
|
// CLOUDICON — Brass Line cloud / haze / overcast glyphs.
|
||||||
// No rim, no disc. Day/night aware: when elev < 0, the sun is
|
// No sun or moon here; the time-cell SkyScope owns day/night imagery.
|
||||||
// replaced with a phased moon (same logic as SkyScope).
|
|
||||||
// ───────────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────────
|
||||||
// category: 'clear' | 'wispy' | 'scattered' | 'overcast'
|
// category: 'clear' | 'wispy' | 'scattered' | 'overcast'
|
||||||
// elev: solar elevation in degrees (negative = night)
|
// elev: solar elevation in degrees (negative = night)
|
||||||
@@ -322,78 +318,48 @@ export function WindVane({ bearing, size = 30 }) {
|
|||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
export function CloudIcon({ category, size = 30, elev = 90, dt = new Date() }) {
|
export function CloudIcon({ category, size = 30, elev = 90, dt = new Date() }) {
|
||||||
const r = size / 2;
|
const r = size / 2;
|
||||||
const u = r; // half-extent for layout
|
const brass = '#c8922a';
|
||||||
const isNight = elev < -3;
|
const ink = '#2a1a08';
|
||||||
// Phased moon helper — returns an SVG <g> with the moon + shadow.
|
const muted = '#b09870';
|
||||||
// Lit fraction direction matches SkyScope (waxing = lit from right).
|
const sw = Math.max(1.25, size * 0.058);
|
||||||
const renderMoon = (cx, cy, moonR) => {
|
const cap = 'round';
|
||||||
const phase = moonPhaseFraction(dt);
|
const join = 'round';
|
||||||
const phaseOffset = Math.cos(2 * Math.PI * phase) * moonR;
|
const cloudPath = (dx = 0, dy = 0, scale = 1) => {
|
||||||
const moonLitFromRight = phase < 0.5;
|
const x = (n) => r + n * r * scale + dx;
|
||||||
const shadowCx = cx + (moonLitFromRight ? -phaseOffset : phaseOffset);
|
const y = (n) => r + n * r * scale + dy;
|
||||||
const clipId = `cmoon-${size}-${Math.round(cx)}-${Math.round(cy)}-${Math.round(phase * 1000)}`;
|
return `M ${x(-0.76)} ${y(0.25)}
|
||||||
return html`
|
H ${x(0.56)}
|
||||||
<g>
|
C ${x(0.82)} ${y(0.25)}, ${x(0.86)} ${y(-0.08)}, ${x(0.58)} ${y(-0.10)}
|
||||||
<defs>
|
C ${x(0.48)} ${y(-0.44)}, ${x(0.08)} ${y(-0.52)}, ${x(-0.10)} ${y(-0.22)}
|
||||||
<clipPath id=${clipId}>
|
C ${x(-0.42)} ${y(-0.33)}, ${x(-0.76)} ${y(-0.08)}, ${x(-0.76)} ${y(0.25)} Z`;
|
||||||
<circle cx=${cx} cy=${cy} r=${moonR} />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<circle cx=${cx} cy=${cy} r=${moonR} fill="#f5edd6" stroke="#9a8c6a" stroke-width="0.4" />
|
|
||||||
<circle cx=${shadowCx} cy=${cy} r=${moonR} fill="#2a2438" clip-path=${`url(#${clipId})`} />
|
|
||||||
<circle cx=${cx} cy=${cy} r=${moonR} fill="none" stroke="#9a8c6a" stroke-width="0.4" />
|
|
||||||
</g>`;
|
|
||||||
};
|
};
|
||||||
|
const line = (x1, y1, x2, y2, color = muted, width = sw, opacity = 1) => html`
|
||||||
|
<line x1=${x1} y1=${y1} x2=${x2} y2=${y2}
|
||||||
|
stroke=${color} stroke-width=${width} stroke-linecap=${cap} opacity=${opacity} />`;
|
||||||
|
const mist = (y0, color = muted) => html`
|
||||||
|
<g>
|
||||||
|
${line(size * 0.18, y0, size * 0.80, y0, color, sw * 0.9, 0.88)}
|
||||||
|
${line(size * 0.30, y0 + size * 0.18, size * 0.92, y0 + size * 0.18, ink, sw * 0.82, 0.72)}
|
||||||
|
</g>`;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<svg width=${size} height=${size} viewBox=${`0 0 ${size} ${size}`}
|
<svg width=${size} height=${size} viewBox=${`0 0 ${size} ${size}`}
|
||||||
style=${{ flexShrink: 0, display: 'inline-block', verticalAlign: 'middle', overflow: 'visible' }}>
|
style=${{ flexShrink: 0, display: 'inline-block', verticalAlign: 'middle', overflow: 'visible' }}>
|
||||||
${category === 'clear' && (isNight
|
${category === 'clear' && html`
|
||||||
? html`<!-- clear night: phased moon, no rays -->
|
${line(size * 0.26, size * 0.50, size * 0.74, size * 0.50, brass, sw, 0.9)}
|
||||||
${renderMoon(r, r, u*0.50)}`
|
${line(size * 0.38, size * 0.64, size * 0.62, size * 0.64, muted, sw * 0.75, 0.7)}`}
|
||||||
: html`<!-- clear day: bright sun with rays -->
|
|
||||||
<g>
|
|
||||||
${[0,45,90,135,180,225,270,315].map(a => {
|
|
||||||
const x1 = r + Math.sin(a*Math.PI/180) * u*0.65;
|
|
||||||
const y1 = r - Math.cos(a*Math.PI/180) * u*0.65;
|
|
||||||
const x2 = r + Math.sin(a*Math.PI/180) * u*0.95;
|
|
||||||
const y2 = r - Math.cos(a*Math.PI/180) * u*0.95;
|
|
||||||
return html`<line key=${a} x1=${x1} y1=${y1} x2=${x2} y2=${y2}
|
|
||||||
stroke="#e6a32a" stroke-width=${Math.max(1, size*0.05)}
|
|
||||||
stroke-linecap="round" />`;
|
|
||||||
})}
|
|
||||||
<circle cx=${r} cy=${r} r=${u*0.45} fill="#ffd84a" stroke="#e6a32a" stroke-width="0.6" />
|
|
||||||
</g>`)}
|
|
||||||
${category === 'wispy' && html`
|
${category === 'wispy' && html`
|
||||||
<!-- sun OR moon with thin cirrus streaks across it -->
|
${mist(size * 0.38, brass)}
|
||||||
${isNight
|
${line(size * 0.18, size * 0.78, size * 0.60, size * 0.78, muted, sw * 0.7, 0.6)}`}
|
||||||
? renderMoon(r, r*0.95, u*0.42)
|
|
||||||
: html`<circle cx=${r} cy=${r*0.95} r=${u*0.42} fill="#ffd84a" stroke="#e6a32a" stroke-width="0.6" />`}
|
|
||||||
<path d=${`M ${r-u*0.95} ${r*0.85} Q ${r} ${r*0.65} ${r+u*0.95} ${r*0.95}`}
|
|
||||||
stroke=${isNight ? '#cfd6dd' : '#ffffff'} stroke-width=${Math.max(1.4, size*0.07)}
|
|
||||||
fill="none" stroke-linecap="round" opacity="0.92" />
|
|
||||||
<path d=${`M ${r-u*0.75} ${r*1.25} Q ${r} ${r*1.08} ${r+u*0.85} ${r*1.30}`}
|
|
||||||
stroke=${isNight ? '#b8c0c8' : '#e6ebf0'} stroke-width=${Math.max(1.1, size*0.055)}
|
|
||||||
fill="none" stroke-linecap="round" opacity="0.85" />`}
|
|
||||||
${category === 'scattered' && html`
|
${category === 'scattered' && html`
|
||||||
<!-- sun OR moon behind a fluffy cumulus cloud -->
|
<path d=${cloudPath(0, 0, 0.82)} fill="none" stroke=${ink}
|
||||||
${isNight
|
stroke-width=${sw} stroke-linecap=${cap} stroke-linejoin=${join} />
|
||||||
? renderMoon(r+u*0.45, r-u*0.40, u*0.32)
|
${line(size * 0.20, size * 0.78, size * 0.48, size * 0.78, brass, sw * 0.75, 0.78)}`}
|
||||||
: html`<circle cx=${r+u*0.45} cy=${r-u*0.40} r=${u*0.34} fill="#ffd84a" stroke="#e6a32a" stroke-width="0.6" />`}
|
|
||||||
<g fill=${isNight ? '#b8c0c8' : '#ffffff'} stroke=${isNight ? '#5a6470' : '#8a96a4'} stroke-width="0.6" stroke-linejoin="round">
|
|
||||||
<ellipse cx=${r-u*0.35} cy=${r+u*0.10} rx=${u*0.50} ry=${u*0.30} />
|
|
||||||
<ellipse cx=${r+u*0.10} cy=${r-u*0.05} rx=${u*0.46} ry=${u*0.36} />
|
|
||||||
<ellipse cx=${r+u*0.40} cy=${r+u*0.20} rx=${u*0.44} ry=${u*0.28} />
|
|
||||||
<!-- flat-ish base, soft underneath -->
|
|
||||||
<rect x=${r-u*0.75} y=${r+u*0.18} width=${u*1.55} height=${u*0.20} rx=${u*0.10} />
|
|
||||||
</g>`}
|
|
||||||
${category === 'overcast' && html`
|
${category === 'overcast' && html`
|
||||||
<!-- thick layered stratus, no sun OR moon (would be hidden anyway) -->
|
<path d=${cloudPath(-size * 0.02, -size * 0.10, 0.70)} fill="none" stroke=${muted}
|
||||||
<g fill=${isNight ? '#7a828c' : '#d8dee5'} stroke=${isNight ? '#3a424c' : '#6e7a88'} stroke-width="0.7" stroke-linejoin="round">
|
stroke-width=${sw * 0.9} stroke-linecap=${cap} stroke-linejoin=${join} opacity="0.82" />
|
||||||
<ellipse cx=${r-u*0.40} cy=${r-u*0.20} rx=${u*0.55} ry=${u*0.32} />
|
<path d=${cloudPath(0, size * 0.14, 0.88)} fill="none" stroke=${ink}
|
||||||
<ellipse cx=${r+u*0.20} cy=${r-u*0.40} rx=${u*0.48} ry=${u*0.30} />
|
stroke-width=${sw} stroke-linecap=${cap} stroke-linejoin=${join} />`}
|
||||||
<ellipse cx=${r+u*0.40} cy=${r+u*0.10} rx=${u*0.55} ry=${u*0.34} />
|
|
||||||
<ellipse cx=${r-u*0.10} cy=${r+u*0.30} rx=${u*0.60} ry=${u*0.32} />
|
|
||||||
</g>`}
|
|
||||||
</svg>`;
|
</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,18 +592,18 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g
|
|||||||
</text>
|
</text>
|
||||||
<text x=${cx} y=${cy + 15} text-anchor="middle"
|
<text x=${cx} y=${cy + 15} text-anchor="middle"
|
||||||
fill=${readoutMutedColor} font-size="6.5"
|
fill=${readoutMutedColor} font-size="6.5"
|
||||||
font-family="JetBrains Mono, monospace" letter-spacing="2">
|
font-family="Manrope, sans-serif" font-weight="700" letter-spacing="1.2">
|
||||||
UTCI NOW
|
UTCI NOW
|
||||||
</text>
|
</text>
|
||||||
<text x=${cx} y=${cy + 23} text-anchor="middle"
|
<text x=${cx} y=${cy + 23} text-anchor="middle"
|
||||||
fill=${glowColor} font-size="7"
|
fill=${glowColor} font-size="7"
|
||||||
font-family="JetBrains Mono, monospace" letter-spacing="0.8">
|
font-family="Manrope, sans-serif" font-weight="700" letter-spacing="0.5">
|
||||||
${cat.label.toUpperCase()}
|
${cat.label.toUpperCase()}
|
||||||
</text>
|
</text>
|
||||||
</>`
|
</>`
|
||||||
: html`<text x=${cx} y=${cy + 5} text-anchor="middle"
|
: html`<text x=${cx} y=${cy + 5} text-anchor="middle"
|
||||||
fill=${readoutMutedColor} font-size="8"
|
fill=${readoutMutedColor} font-size="8"
|
||||||
font-family="JetBrains Mono, monospace" letter-spacing="1.2">
|
font-family="Manrope, sans-serif" font-weight="700" letter-spacing="0.8">
|
||||||
AWAITING
|
AWAITING
|
||||||
</text>`}
|
</text>`}
|
||||||
</svg>`;
|
</svg>`;
|
||||||
@@ -664,25 +630,28 @@ export function PrecipIcon({ precip = 0, snow = 0, size = 28 }) {
|
|||||||
const hasRain = precip > 0;
|
const hasRain = precip > 0;
|
||||||
const hasSnow = snow > 0;
|
const hasSnow = snow > 0;
|
||||||
const mixed = hasRain && hasSnow;
|
const mixed = hasRain && hasSnow;
|
||||||
|
const brass = '#c8922a';
|
||||||
// Drop shape: teardrop path centred on (cx, cy), radius dr
|
const ink = '#2a1a08';
|
||||||
const drop = (cx, cy, dr) => {
|
const rain = '#2a6a90';
|
||||||
const tip = cy + dr * 1.55;
|
const snowBlue = '#3f73c4';
|
||||||
const cw = dr * 0.72;
|
const sw = Math.max(1.25, size * 0.055);
|
||||||
return `M ${cx} ${tip} C ${cx - cw} ${cy + dr * 0.6}, ${cx - dr} ${cy - dr * 0.3}, ${cx} ${cy - dr} C ${cx + dr} ${cy - dr * 0.3}, ${cx + cw} ${cy + dr * 0.6}, ${cx} ${tip} Z`;
|
const cloud = html`
|
||||||
};
|
<path d=${`M ${size*0.18} ${size*0.36}
|
||||||
|
H ${size*0.72}
|
||||||
// Six-pointed snowflake: centre lines + diagonal lines
|
C ${size*0.86} ${size*0.36}, ${size*0.88} ${size*0.20}, ${size*0.72} ${size*0.19}
|
||||||
|
C ${size*0.66} ${size*0.04}, ${size*0.42} ${size*0.02}, ${size*0.34} ${size*0.17}
|
||||||
|
C ${size*0.22} ${size*0.14}, ${size*0.14} ${size*0.24}, ${size*0.18} ${size*0.36} Z`}
|
||||||
|
fill="none" stroke=${ink} stroke-width=${sw} stroke-linecap="round" stroke-linejoin="round" />`;
|
||||||
|
const rainStroke = (cx, cy, heavy = false) => html`
|
||||||
|
<line x1=${cx + size * 0.04} y1=${cy} x2=${cx - size * 0.04} y2=${cy + size * 0.18}
|
||||||
|
stroke=${rain} stroke-width=${heavy ? sw * 1.1 : sw}
|
||||||
|
stroke-linecap="round" />`;
|
||||||
const flake = (cx, cy, fr) => html`
|
const flake = (cx, cy, fr) => html`
|
||||||
<g stroke="#6090c8" stroke-width=${Math.max(1, size * 0.055)} stroke-linecap="round">
|
<g stroke=${snowBlue} stroke-width=${Math.max(1, size * 0.045)} stroke-linecap="round">
|
||||||
<line x1=${cx - fr} y1=${cy} x2=${cx + fr} y2=${cy} />
|
<line x1=${cx - fr} y1=${cy} x2=${cx + fr} y2=${cy} />
|
||||||
<line x1=${cx} y1=${cy - fr} x2=${cx} y2=${cy + fr} />
|
<line x1=${cx} y1=${cy - fr} x2=${cx} y2=${cy + fr} />
|
||||||
<line x1=${cx - fr * 0.7} y1=${cy - fr * 0.7} x2=${cx + fr * 0.7} y2=${cy + fr * 0.7} />
|
<line x1=${cx - fr * 0.68} y1=${cy - fr * 0.68} x2=${cx + fr * 0.68} y2=${cy + fr * 0.68} />
|
||||||
<line x1=${cx + fr * 0.7} y1=${cy - fr * 0.7} x2=${cx - fr * 0.7} y2=${cy + fr * 0.7} />
|
<line x1=${cx + fr * 0.68} y1=${cy - fr * 0.68} x2=${cx - fr * 0.68} y2=${cy + fr * 0.68} />
|
||||||
${[-1, 1].map(sx => [-1, 1].map(sy => html`
|
|
||||||
<line key=${`${sx}${sy}`}
|
|
||||||
x1=${cx + sx * fr * 0.55} y1=${cy + sy * 0}
|
|
||||||
x2=${cx + sx * fr * 0.75} y2=${cy + sy * fr * 0.3} />`))}
|
|
||||||
</g>`;
|
</g>`;
|
||||||
|
|
||||||
// Dry — just a faint dash
|
// Dry — just a faint dash
|
||||||
@@ -691,7 +660,7 @@ export function PrecipIcon({ precip = 0, snow = 0, size = 28 }) {
|
|||||||
<svg width=${size} height=${size} viewBox=${`0 0 ${size} ${size}`}
|
<svg width=${size} height=${size} viewBox=${`0 0 ${size} ${size}`}
|
||||||
style=${{ flexShrink: 0, display: 'inline-block', verticalAlign: 'middle' }}>
|
style=${{ flexShrink: 0, display: 'inline-block', verticalAlign: 'middle' }}>
|
||||||
<line x1=${r - size * 0.2} y1=${r} x2=${r + size * 0.2} y2=${r}
|
<line x1=${r - size * 0.2} y1=${r} x2=${r + size * 0.2} y2=${r}
|
||||||
stroke="#c0a880" stroke-width="1.5" stroke-linecap="round" />
|
stroke=${brass} stroke-width="1.5" stroke-linecap="round" opacity="0.75" />
|
||||||
</svg>`;
|
</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,9 +668,7 @@ export function PrecipIcon({ precip = 0, snow = 0, size = 28 }) {
|
|||||||
const rainDrops = !hasRain ? 0 : precip < 1 ? 1 : precip < 4 ? 2 : 3;
|
const rainDrops = !hasRain ? 0 : precip < 1 ? 1 : precip < 4 ? 2 : 3;
|
||||||
// Snow intensity → number of flakes
|
// Snow intensity → number of flakes
|
||||||
const snowFlakes = !hasSnow ? 0 : snow < 0.5 ? 1 : 2;
|
const snowFlakes = !hasSnow ? 0 : snow < 0.5 ? 1 : 2;
|
||||||
const dropColor = '#5090b0';
|
const fr = size * 0.08; // flake arm length
|
||||||
const dr = size * 0.13; // drop radius
|
|
||||||
const fr = size * 0.18; // flake arm length
|
|
||||||
|
|
||||||
// Layout: spread items evenly across the icon width
|
// Layout: spread items evenly across the icon width
|
||||||
const items = (mixed ? rainDrops + snowFlakes : hasRain ? rainDrops : snowFlakes);
|
const items = (mixed ? rainDrops + snowFlakes : hasRain ? rainDrops : snowFlakes);
|
||||||
@@ -710,20 +677,23 @@ export function PrecipIcon({ precip = 0, snow = 0, size = 28 }) {
|
|||||||
return html`
|
return html`
|
||||||
<svg width=${size} height=${size} viewBox=${`0 0 ${size} ${size}`}
|
<svg width=${size} height=${size} viewBox=${`0 0 ${size} ${size}`}
|
||||||
style=${{ flexShrink: 0, display: 'inline-block', verticalAlign: 'middle', overflow: 'visible' }}>
|
style=${{ flexShrink: 0, display: 'inline-block', verticalAlign: 'middle', overflow: 'visible' }}>
|
||||||
|
${cloud}
|
||||||
${mixed
|
${mixed
|
||||||
? html`
|
? html`
|
||||||
${Array.from({ length: rainDrops }, (_, i) => html`
|
${Array.from({ length: rainDrops }, (_, i) => html`
|
||||||
<path key=${'d' + i} d=${drop(spacing * (i + 1), r - dr * 0.3, dr)}
|
<${Fragment} key=${'d' + i}>
|
||||||
fill=${dropColor} opacity="0.9" />`)}
|
${rainStroke(spacing * (i + 1), size * 0.52, false)}
|
||||||
|
</>`)}
|
||||||
${Array.from({ length: snowFlakes }, (_, i) =>
|
${Array.from({ length: snowFlakes }, (_, i) =>
|
||||||
flake(spacing * (rainDrops + i + 1), r, fr)
|
flake(spacing * (rainDrops + i + 1), size * 0.68, fr)
|
||||||
)}`
|
)}`
|
||||||
: hasRain
|
: hasRain
|
||||||
? Array.from({ length: rainDrops }, (_, i) => html`
|
? Array.from({ length: rainDrops }, (_, i) => html`
|
||||||
<path key=${i} d=${drop(spacing * (i + 1), r - dr * 0.3, dr)}
|
<${Fragment} key=${i}>
|
||||||
fill=${dropColor} opacity="0.9" />`)
|
${rainStroke(spacing * (i + 1), size * 0.53, precip >= 4)}
|
||||||
|
</>`)
|
||||||
: Array.from({ length: snowFlakes }, (_, i) =>
|
: Array.from({ length: snowFlakes }, (_, i) =>
|
||||||
flake(spacing * (i + 1), r, fr)
|
flake(spacing * (i + 1), size * 0.66, fr)
|
||||||
)}
|
)}
|
||||||
</svg>`;
|
</svg>`;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -133,10 +133,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'Manrope', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.07em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
button:hover { background: #a06e18; }
|
button:hover { background: #a06e18; }
|
||||||
@@ -153,10 +154,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
.back {
|
.back {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'Manrope', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
.back a { color: #b09870; text-decoration: none; }
|
.back a { color: #b09870; text-decoration: none; }
|
||||||
.back a:hover { color: #c8922a; }
|
.back a:hover { color: #c8922a; }
|
||||||
|
|||||||
Reference in New Issue
Block a user