Update desktop profile hover button
Recalibrated vehicle ventilation calcs
This commit is contained in:
fraxle
2026-07-26 16:03:53 +01:00
parent 84d035d903
commit dbadb1d410
10 changed files with 271 additions and 81 deletions
+30 -1
View File
@@ -55,6 +55,7 @@ export function ConfigPanel({
}) {
const profileScrollRef = useRef(null);
const profileWrapRef = useRef(null);
const dropdownRef = useRef(null);
const { mainConfigKey, mainLabel } = deriveProfileMain(activeProfile, outdoorsVariant);
@@ -77,6 +78,34 @@ export function ConfigPanel({
return () => document.removeEventListener('keydown', onKey);
}, [open, onClose]);
// From tablet up the panel drops *out of* the active-profile-row rather
// than sliding up from the floating button (which is mobile-only), so it
// has to be anchored to that row's live bottom edge — it's sticky on
// desktop and scrolls away on tablet, hence remeasuring on scroll/resize.
// Mobile (<=640px) ignores the var entirely and keeps the bottom sheet.
useEffect(() => {
const el = dropdownRef.current;
if (!el) return;
const measure = () => {
const row = document.querySelector('.active-profile-row');
const top = row ? Math.max(0, row.getBoundingClientRect().bottom) : 0;
el.style.setProperty('--profile-anchor-top', `${Math.round(top)}px`);
// Not enough room below the row for the panel's content? Fall back to
// the mobile bottom-sheet layout, which gets the full viewport height.
const body = el.querySelector('.profile-dropdown-body');
const content = body ? body.scrollHeight : 0;
el.classList.toggle('is-bottom-sheet', content > window.innerHeight - top - 16);
};
measure();
if (!open) return;
window.addEventListener('scroll', measure, true);
window.addEventListener('resize', measure);
return () => {
window.removeEventListener('scroll', measure, true);
window.removeEventListener('resize', measure);
};
}, [open, activeTab, activeProfile, outdoorsVariant]);
// Drag-to-scroll + fade edges for the horizontal profile card row.
useEffect(() => {
const el = profileScrollRef.current;
@@ -226,7 +255,7 @@ export function ConfigPanel({
return html`
<${Fragment}>
<div class=${`profile-dropdown-overlay${open ? ' is-open' : ''}`} onClick=${onClose}></div>
<div class=${`profile-dropdown${open ? ' is-open' : ''}`} role="dialog" aria-modal="true" aria-label="Profile and configuration" aria-hidden=${!open}>
<div ref=${dropdownRef} class=${`profile-dropdown${open ? ' is-open' : ''}`} role="dialog" aria-modal="true" aria-label="Profile and configuration" aria-hidden=${!open}>
<button class="profile-dropdown-close" aria-label="Close" onClick=${onClose}>×</button>
<div class="profile-dropdown-body">
+7 -4
View File
@@ -35,7 +35,7 @@ import { h, Fragment } from '../../vendor/preact.js';
import htm from '../../vendor/htm.js';
import { confidenceBand, utciCategory, VEHICLE_SPEEDS, VEHICLE_TYPES, BUILDING_TYPES, FUR_COLORS } from '../utils.js';
import { FREE_DAYS, UTCI_ENVIRONMENTS, deriveProfileMain, FILTER_PROFILES, variantIcons } from '../config.js';
import { calcVehicleInteriorTemp } from '../physics.js';
import { calcVehicleInteriorTempPass } from '../physics.js';
import { CloudIcon, PrecipIcon, HouseIcon, CarIcon, FogIcon, IceIcon } from '../components.js';
import { SubscribeModal } from './SubscribeModal.js';
@@ -222,9 +222,12 @@ export function DayTabs({
let shadeHi = null, shadeLo = null;
if (isVehicleProfile) {
const speedMph = (VEHICLE_SPEEDS[vehicleSpeed] ?? VEHICLE_SPEEDS.static).mph;
const ventVals = d.rows
.map(r => calcVehicleInteriorTemp(r.Ta, r.glob, r.elev, vehicleType, true, speedMph))
.filter(v => isFinite(v));
// Run the lagged pass over the whole day, not per-hour equilibrium,
// so this hi/lo is directly comparable with the vehicleT column.
const ventVals = calcVehicleInteriorTempPass(
d.rows.map(r => r.Ta), d.rows.map(r => r.glob), d.rows.map(r => r.elev),
d.rows.map(r => r.va), vehicleType, true, speedMph
).filter(v => v != null && isFinite(v));
shadeHi = ventVals.length ? Math.round(Math.max(...ventVals)) : null;
shadeLo = ventVals.length ? Math.round(Math.min(...ventVals)) : null;
} else if (secondaryField) {
+1 -7
View File
@@ -23,13 +23,7 @@ import { Wordmark } from './Wordmark.js';
const html = htm.bind(h);
const SUBSCRIBE_URL = 'https://buy.stripe.com/9B63cw7vl8k15Ei9DQd7q00';
// TODO: replace with the one-time Payment Link created in the Stripe
// Dashboard (see plan Part 3) - a flat-rate product with a few selectable
// price options (e.g. £3 / £5 / £10), since Stripe Payment Links don't
// support true customer-chosen amounts. Configure its after-payment
// redirect to https://sunscope.net/?session_id={CHECKOUT_SESSION_ID}
// the same way the monthly link should be.
const SUBSCRIBE_URL_ONEOFF = 'https://buy.stripe.com/REPLACE_WITH_ONEOFF_PAYMENT_LINK';
const SUBSCRIBE_URL_ONEOFF = 'https://buy.stripe.com/14A14odTJ6bT0jY4jwd7q02';
const MANAGE_URL = 'https://billing.stripe.com/p/login/9B63cw7vl8k15Ei9DQd7q00';
export function SubscribeModal({ title, detail, onClose, openRestore }) {