Integrate fonts
Add office profile
Fix onload pulldown selections
This commit is contained in:
fraxle
2026-05-25 10:23:51 +01:00
parent e9a6cc9912
commit 2cb4ebe52b
20 changed files with 185 additions and 52 deletions
+12
View File
@@ -1,3 +1,15 @@
/* ── SELF-HOSTED FONTS ───────────────────────────────────────────────── */
@font-face { font-family: 'Manrope'; font-style: normal; font-weight: 400; font-display: swap; src: url('../fonts/manrope-400.woff2') format('woff2'); }
@font-face { font-family: 'Manrope'; font-style: normal; font-weight: 500; font-display: swap; src: url('../fonts/manrope-500.woff2') format('woff2'); }
@font-face { font-family: 'Manrope'; font-style: normal; font-weight: 600; font-display: swap; src: url('../fonts/manrope-600.woff2') format('woff2'); }
@font-face { font-family: 'Manrope'; font-style: normal; font-weight: 700; font-display: swap; src: url('../fonts/manrope-700.woff2') format('woff2'); }
@font-face { font-family: 'Fraunces'; font-style: normal; font-weight: 700; font-display: swap; src: url('../fonts/fraunces-700-normal.woff2') format('woff2'); }
@font-face { font-family: 'Fraunces'; font-style: normal; font-weight: 900; font-display: swap; src: url('../fonts/fraunces-900-normal.woff2') format('woff2'); }
@font-face { font-family: 'Fraunces'; font-style: italic; font-weight: 300; font-display: swap; src: url('../fonts/fraunces-300-italic.woff2') format('woff2'); }
@font-face { font-family: 'Fraunces'; font-style: italic; font-weight: 700; font-display: swap; src: url('../fonts/fraunces-700-italic.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('../fonts/jetbrains-mono-400.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 600; font-display: swap; src: url('../fonts/jetbrains-mono-600.woff2') format('woff2'); }
/* ════════════════════════════════════════════════════════════════════════
base.css — Fonts, reset, page shell, and top navigation.
════════════════════════════════════════════════════════════════════════
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5 -3
View File
@@ -54,8 +54,8 @@ export function UTCIForecast() {
visibleCols, setVisibleCols, toggleCol,
showDecimals, toggleShowDecimals,
showUnits, toggleShowUnits,
activityOptions, placeOptions,
activityValue, activityLabel, placeValue, placeLabel,
activityOptions, placeOptions, workOptions,
activityValue, activityLabel, placeValue, placeLabel, workValue, workLabel,
skinType, setSkinType,
vehicleType, setVehicleType,
vehicleVent, setVehicleVent,
@@ -321,12 +321,14 @@ export function UTCIForecast() {
proPromptSource=${proPromptSource} setProPromptSource=${setProPromptSource}
activeProfile=${activeProfile} activateProfile=${activateProfile} activeCols=${activeCols}
visibleCols=${visibleCols}
activityOptions=${activityOptions} placeOptions=${placeOptions}
activityOptions=${activityOptions} placeOptions=${placeOptions} workOptions=${workOptions}
activityValue=${activityValue} activityLabel=${activityLabel}
placeValue=${placeValue} placeLabel=${placeLabel}
workValue=${workValue} workLabel=${workLabel}
outdoorsVariant=${outdoorsVariant} setOutdoorsVariantAndSave=${setOutdoorsVariantAndSave}
setActiveProfile=${setActiveProfile} setVisibleCols=${setVisibleCols}
setIndoorMode=${setIndoorMode} setIndoorManaged=${setIndoorManaged}
setBuildingType=${setBuildingType}
dayTabsRef=${dayTabsRef} canScrollLeft=${canScrollLeft} canScrollRight=${canScrollRight}
scrollDayTabs=${scrollDayTabs}
/>`}
+39 -6
View File
@@ -22,10 +22,13 @@
// visibleCols - object of col-key -> boolean visibility
// activityOptions - dropdown options for Activities selector
// placeOptions - dropdown options for Places selector
// workOptions - dropdown options for Work selector
// activityValue - current activity dropdown value
// activityLabel - current activity dropdown label
// placeValue - current place dropdown value
// placeLabel - current place dropdown label
// workValue - current work dropdown value
// workLabel - current work dropdown label
// outdoorsVariant - current outdoors sub-variant key
// setOutdoorsVariantAndSave - setter that also persists to localStorage
// setActiveProfile - raw setter for activeProfile
@@ -41,7 +44,7 @@
import { h, Fragment } from '../../vendor/preact.js';
import htm from '../../vendor/htm.js';
import { confidenceBand } from '../utils.js';
import { FREE_DAYS, FILTER_PROFILES, OUTDOORS_VARIANTS, profileButtonOrder, activityVariantKeys, placeVariantKeys } from '../config.js';
import { FREE_DAYS, FILTER_PROFILES, OUTDOORS_VARIANTS, profileButtonOrder, activityVariantKeys, placeVariantKeys, workVariantKeys } from '../config.js';
import { CloudIcon, PrecipIcon, CustomSelect } from '../components.js';
const html = htm.bind(h);
@@ -54,12 +57,13 @@ export function DayTabs({
proPromptSource, setProPromptSource,
activeProfile, activateProfile, activeCols,
visibleCols,
activityOptions, placeOptions,
activityOptions, placeOptions, workOptions,
activityValue, activityLabel,
placeValue, placeLabel,
workValue, workLabel,
outdoorsVariant, setOutdoorsVariantAndSave,
setActiveProfile, setVisibleCols,
setIndoorMode, setIndoorManaged,
setIndoorMode, setIndoorManaged, setBuildingType,
dayTabsRef, canScrollLeft, canScrollRight, scrollDayTabs,
}) {
return html`
@@ -398,11 +402,34 @@ export function DayTabs({
<${CustomSelect}
key="activities"
value=${activityValue}
isOn=${activeProfile === 'farming' || (activeProfile === 'outdoors' && activityVariantKeys.includes(outdoorsVariant))}
isOn=${activeProfile === 'outdoors' && activityVariantKeys.includes(outdoorsVariant)}
noHide=${true}
hideLabel="🎯 Activities"
buttonLabel=${activityValue === 'off' ? '🎯 Activities' : ` ${activityLabel}`}
options=${activityOptions}
onChange=${(v) => {
if (v === 'off') { activateProfile('basic'); return; }
if (OUTDOORS_VARIANTS[v]?.proOnly && !isPro) {
setProPromptSource(`variant:${v}`);
setProPromptDay(0);
return;
}
try { localStorage.setItem('sunscope_profile', 'outdoors'); } catch (e) { /* ignore */ }
setActiveProfile('outdoors');
setOutdoorsVariantAndSave(v);
setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols });
setIndoorMode('off');
setIndoorManaged(false);
}}
/>
<${CustomSelect}
key="work"
value=${workValue}
isOn=${activeProfile === 'farming' || (activeProfile === 'outdoors' && workVariantKeys.includes(outdoorsVariant))}
noHide=${true}
hideLabel="💼 Work"
buttonLabel=${workValue === 'off' ? '💼 Work' : ` ${workLabel}`}
options=${workOptions}
onChange=${(v) => {
if (v === 'off') { activateProfile('basic'); return; }
if (v === 'farming') { activateProfile('farming'); return; }
@@ -415,8 +442,14 @@ export function DayTabs({
setActiveProfile('outdoors');
setOutdoorsVariantAndSave(v);
setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols });
setIndoorMode('off');
setIndoorManaged(false);
if (v === 'office') {
setBuildingType('office');
setIndoorMode('on');
setIndoorManaged(false);
} else {
setIndoorMode('off');
setIndoorManaged(false);
}
}}
/>
<span class="profile-divider" aria-hidden="true"></span>
+12 -2
View File
@@ -94,6 +94,10 @@ export const OUTDOORS_VARIANTS = {
hiking: { name: 'Hiking', proOnly: true, cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: true, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } },
photography: { name: 'Photography', proOnly: true, cols: { hour: true, air: true, rh: false, dew: false, wind: false, dir: false, cloud: true, sun: true, direct: true, diffuse: true, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } },
fishing: { name: 'Fishing', proOnly: true, cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } },
// Work variants
market: { name: 'Market Trading', cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } },
windowcleaning: { name: 'Window Cleaning', proOnly: true, cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } },
office: { name: 'Office', proOnly: true, cols: { hour: true, air: true, rh: true, dew: false, wind: false, dir: false, cloud: true, sun: true, direct: true, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: true, vis: false, aqi: true, pollen: false } },
};
// Pollen type options shown in the pulldown selector.
@@ -128,13 +132,19 @@ export const variantIcons = {
hiking: '🏔️',
photography: '📸',
fishing: '🎣',
market: '🏪',
windowcleaning: '🪟',
office: '🏢',
};
// Which variant keys appear in the Activities dropdown.
export const activityVariantKeys = ['cycling', 'running', 'dogwalk', 'sailing', 'wintersports', 'naturist', 'hiking', 'photography', 'fishing'];
export const activityVariantKeys = ['dogwalk', 'running', 'cycling', 'hiking', 'fishing', 'sailing', 'wintersports', 'photography', 'naturist'];
// Which variant keys appear in the Places dropdown.
export const placeVariantKeys = ['urban', 'beach', 'events', 'park', 'airport', 'festival', 'construction'];
export const placeVariantKeys = ['urban', 'park', 'beach', 'events', 'festival', 'airport'];
// Which variant keys appear in the Work dropdown.
export const workVariantKeys = ['farming', 'construction', 'market', 'windowcleaning', 'office'];
// Tooltip text shown when the user clicks/hovers a table column header.
export const COL_DESCRIPTIONS = {
+78 -32
View File
@@ -26,7 +26,7 @@ import { useState, useEffect, useRef, useCallback } from '../../vendor/preact-ho
import {
FREE_DAYS, FILTER_PROFILES, OUTDOORS_VARIANTS,
profileButtonOrder, variantIcons,
activityVariantKeys, placeVariantKeys,
activityVariantKeys, placeVariantKeys, workVariantKeys,
} from '../config.js';
import { utciCategory } from '../utils.js';
import { buildHourlyRows } from '../compute.js';
@@ -167,8 +167,20 @@ export function useAppState() {
// ── 5. SKIN, VEHICLE, INDOOR, POLLEN ─────────────────────────────────
const [skinType, setSkinType] = useState('II');
const [vehicleType, setVehicleType] = useState('car');
const [vehicleVent, setVehicleVent] = useState(false);
const [vehicleType, setVehicleType] = useState(() => {
try { return localStorage.getItem('sunscope_vehicle_type') || 'car'; } catch (e) { return 'car'; }
});
const setVehicleTypeAndSave = (v) => {
try { localStorage.setItem('sunscope_vehicle_type', v); } catch (e) { /* ignore */ }
setVehicleType(v);
};
const [vehicleVent, setVehicleVent] = useState(() => {
try { return localStorage.getItem('sunscope_vehicle_vent') === '1'; } catch (e) { return false; }
});
const setVehicleVentAndSave = (v) => {
try { localStorage.setItem('sunscope_vehicle_vent', v ? '1' : '0'); } catch (e) { /* ignore */ }
setVehicleVent(v);
};
const [outdoorsVariant, setOutdoorsVariant] = useState(() => {
try { return localStorage.getItem('sunscope_outdoors_variant') || 'urban'; } catch (e) { return 'urban'; }
@@ -190,15 +202,40 @@ export function useAppState() {
? (OUTDOORS_VARIANTS[outdoorsVariant]?.cols ?? FILTER_PROFILES.outdoors.cols)
: (FILTER_PROFILES[activeProfile]?.cols ?? FILTER_PROFILES.basic.cols);
const [buildingType, setBuildingType] = useState('brick');
const [indoorManaged, setIndoorManaged] = useState(false);
const [indoorMode, setIndoorMode] = useState(() => {
const [buildingType, setBuildingType] = useState(() => {
try {
const saved = localStorage.getItem('sunscope_profile') || 'basic';
const cols = FILTER_PROFILES[saved]?.cols ?? FILTER_PROFILES.basic.cols;
const saved = localStorage.getItem('sunscope_building_type');
if (saved) return saved;
// Fall back to office if the saved variant is office and no explicit building saved
const savedVariant = localStorage.getItem('sunscope_outdoors_variant') || '';
if (savedVariant === 'office') return 'office';
} catch (e) { /* ignore */ }
return 'brick';
});
const setBuildingTypeAndSave = (v) => {
try { localStorage.setItem('sunscope_building_type', v); } catch (e) { /* ignore */ }
setBuildingType(v);
};
const [indoorManaged, setIndoorManaged] = useState(() => {
try { return localStorage.getItem('sunscope_indoor_managed') === '1'; } catch (e) { return false; }
});
const setIndoorManagedAndSave = (v) => {
try { localStorage.setItem('sunscope_indoor_managed', v ? '1' : '0'); } catch (e) { /* ignore */ }
setIndoorManaged(v);
};
const [indoorMode, setIndoorMode] = useState(() => {
try {
const saved = localStorage.getItem('sunscope_indoor_mode');
if (saved) return saved;
const profile = localStorage.getItem('sunscope_profile') || 'basic';
const cols = FILTER_PROFILES[profile]?.cols ?? FILTER_PROFILES.basic.cols;
return (cols['indoorT'] || cols['managedT']) ? 'on' : 'off';
} catch (e) { return 'off'; }
});
const setIndoorModeAndSave = (v) => {
try { localStorage.setItem('sunscope_indoor_mode', v); } catch (e) { /* ignore */ }
setIndoorMode(v);
};
const [pollenType, setPollenType] = useState(() => {
try { return localStorage.getItem('sunscope_pollen_type') || 'all_pollen'; } catch (e) { return 'all_pollen'; }
@@ -233,19 +270,13 @@ export function useAppState() {
const searchTimeout = useRef(null);
// Derived selectors used by profile controls
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 activityOptions = 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 {
@@ -253,17 +284,31 @@ export function useAppState() {
label: `${variantIcons[k]} ${v.name}${v.proOnly && !isPro ? ' 🔒' : ''}`,
};
});
const workOptions = workVariantKeys.map((k) => {
const v = k === 'farming'
? { name: FILTER_PROFILES.farming.label, proOnly: false }
: OUTDOORS_VARIANTS[k];
const icon = k === 'farming' ? FILTER_PROFILES.farming.icon : variantIcons[k];
return {
value: k,
label: `${icon} ${v.name}${v.proOnly && !isPro ? ' 🔒' : ''}`,
};
});
const activityValue = activeProfile === 'farming'
? 'farming'
: activeProfile === 'outdoors' && activityVariantKeys.includes(outdoorsVariant)
? outdoorsVariant
: 'off';
const activityValue = activeProfile === 'outdoors' && activityVariantKeys.includes(outdoorsVariant)
? outdoorsVariant
: 'off';
const activityLabel = activityOptions.find((o) => o.value === activityValue)?.label;
const placeValue = activeProfile === 'outdoors' && placeVariantKeys.includes(outdoorsVariant)
? outdoorsVariant
: 'off';
const placeLabel = placeOptions.find((o) => o.value === placeValue)?.label;
const workValue = activeProfile === 'farming'
? 'farming'
: activeProfile === 'outdoors' && workVariantKeys.includes(outdoorsVariant)
? outdoorsVariant
: 'off';
const workLabel = workOptions.find((o) => o.value === workValue)?.label;
// ── 6. TABLE REFS ────────────────────────────────────────────────────
const headStickyRef = useRef(null);
@@ -439,17 +484,18 @@ export function useAppState() {
activeProfile, setActiveProfile,
activateProfile, activeCols,
visibleCols, setVisibleCols, toggleCol,
activityOptions, placeOptions,
activityOptions, placeOptions, workOptions,
activityValue, activityLabel,
placeValue, placeLabel,
workValue, workLabel,
// skin, vehicle, indoor, pollen
skinType, setSkinType,
vehicleType, setVehicleType,
vehicleVent, setVehicleVent,
vehicleType, setVehicleType: setVehicleTypeAndSave,
vehicleVent, setVehicleVent: setVehicleVentAndSave,
outdoorsVariant, setOutdoorsVariantAndSave,
buildingType, setBuildingType,
indoorManaged, setIndoorManaged,
indoorMode, setIndoorMode,
buildingType, setBuildingType: setBuildingTypeAndSave,
indoorManaged, setIndoorManaged: setIndoorManagedAndSave,
indoorMode, setIndoorMode: setIndoorModeAndSave,
pollenType, setPollenTypeAndSave,
showDecimals, toggleShowDecimals,
showUnits, toggleShowUnits,
+1
View File
@@ -211,6 +211,7 @@ export const BUILDING_TYPES = {
timber: { name: 'Timber Frame / New Build', uWall: 0.22, lagHours: 2, glazingRatio: 0.22, gValue: 0.35, orientFactor: 0.50, curtainBlock: 0.70, ventAlpha: 0.35, solarScale: 0.08, baseTemp: 17.0, internalGain: 0.8, retainedScale: 0.45 },
flat: { name: 'Top-floor Flat', uWall: 0.40, lagHours: 3, glazingRatio: 0.18, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.75, ventAlpha: 0.20, solarScale: 0.11, baseTemp: 17.0, internalGain: 0.9, retainedScale: 0.45 },
conservatory:{ name: 'Conservatory / Sun Room', uWall: 1.20, lagHours: 1, glazingRatio: 0.70, gValue: 0.72, orientFactor: 0.70, curtainBlock: 0.50, ventAlpha: 0.50, solarScale: 0.045, baseTemp: 12.0, internalGain: 0.2, retainedScale: 0.05 },
office: { name: 'Office Block', uWall: 0.25, lagHours: 2, glazingRatio: 0.45, gValue: 0.55, orientFactor: 0.65, curtainBlock: 0.50, ventAlpha: 0.15, solarScale: 0.055, baseTemp: 18.5, internalGain: 2.5, retainedScale: 0.30 },
};
// -------------------------------------------------------------------
+1 -1
View File
@@ -29,5 +29,5 @@
@import "./css/base.css";
@import "./css/header.css";
@import "./css/table.css?v=2";
@import "./css/table.css";
@import "./css/ui.css";