Future Idea Comments

This commit is contained in:
fraxle
2026-05-18 20:38:19 +01:00
parent 9e810e2f8d
commit 31233c713f
4 changed files with 341 additions and 0 deletions
+182
View File
@@ -143,6 +143,122 @@ export function UTCIForecast() {
return localStorage.getItem('sunscope_pro') === '1';
});
// ── FUTURE FEATURE v2: Dark Mode + Theme System (JSON-driven) ───────────────
// Add a `theme` state that drives a data-theme attribute on <body> or .utci-app.
// CSS custom properties do all the heavy lifting — no JS colour logic needed.
//
// THE THEME FILE: /assets/themes.json
// All themes live in a single JSON file. Easy to read, easy to edit —
// add a new theme by copying an existing block and changing the values.
// No build step, no JS changes required for new themes.
//
// Each theme has:
// id — unique key used in localStorage and data-theme attribute
// name — human-readable label (shown in theme picker UI)
// description — one-liner explaining the look/feel
// base — which base mode it extends: "light" | "dark"
// trigger — how/when it activates (see trigger types below)
// colors — the CSS custom properties it overrides (only what changes)
//
// Example themes.json:
// [
// {
// "id": "light",
// "name": "Light (Default)",
// "description": "Classic parchment and brass in full daylight",
// "base": "light",
// "trigger": { "type": "manual" },
// "colors": {
// "--bg": "#f5edd6",
// "--surface": "#ede0c4",
// "--brass": "#c8922a",
// "--text": "#2a1a08",
// "--text-muted": "#9a7d5a",
// "--accent": "#c8922a"
// }
// },
// {
// "id": "dark",
// "name": "Dark",
// "description": "Deep walnut and glowing brass — made for night",
// "base": "dark",
// "trigger": { "type": "manual" },
// "colors": {
// "--bg": "#1a1208",
// "--surface": "#261a0a",
// "--brass": "#c8922a",
// "--text": "#f5edd6",
// "--text-muted": "#9a7d5a",
// "--accent": "#d4a030"
// }
// },
// {
// "id": "christmas",
// "name": "Christmas",
// "description": "Holly green with crimson brass — Dec 20 to Jan 2",
// "base": "dark",
// "trigger": { "type": "date", "from": "12-20", "to": "01-02" },
// "colors": {
// "--bg": "#0f1f0f",
// "--surface": "#1a3a1a",
// "--brass": "#b8312f",
// "--text": "#f0ede0",
// "--text-muted": "#7a9a7a",
// "--accent": "#c8922a"
// }
// },
// {
// "id": "halloween",
// "name": "Halloween",
// "description": "Near-black with amber and burnt orange — Oct 2531",
// "base": "dark",
// "trigger": { "type": "date", "from": "10-25", "to": "10-31" },
// "colors": {
// "--bg": "#100a00",
// "--surface": "#1e1000",
// "--brass": "#cc6600",
// "--text": "#f0d080",
// "--text-muted": "#8a6020",
// "--accent": "#e07820"
// }
// },
// {
// "id": "heatwave",
// "name": "Heatwave Alert",
// "description": "Deep red pulse when a dangerous heat day is forecast",
// "base": "light",
// "trigger": { "type": "alert", "alertType": "heat" },
// "colors": {
// "--brass": "#b82020",
// "--accent": "#c83030"
// }
// }
// ]
//
// TRIGGER TYPES:
// "manual" — user picks it from the theme switcher in the nav, persisted
// to localStorage ('sunscope_theme')
// "date" — auto-applies between MM-DD dates (wraps year-end correctly)
// user can still override manually; auto resets when out of range
// "alert" — activates when daySummary (compute.js) fires a matching alert;
// subtle, temporary — reverts when alert clears
// "system" — mirrors the OS dark/light preference (prefers-color-scheme)
//
// HOW IT WORKS IN JS:
// 1. Fetch /assets/themes.json once on load (or bundle it as an import)
// 2. Walk the trigger rules to find the highest-priority active theme
// Priority: alert > manual > date > system
// 3. Apply the winning theme's colors as inline CSS variables on <body>
// Object.entries(theme.colors).forEach(([k,v]) =>
// document.body.style.setProperty(k, v))
// 4. Store active theme id in localStorage for manual overrides
// 5. A small theme-picker icon (🎨 or ☀️/🌙) in the top nav lets the
// user browse and manually select any theme
//
// const [theme, setTheme] = useState(() =>
// localStorage.getItem('sunscope_theme') || 'system');
// ─────────────────────────────────────────────────────────────────────────────
// (FREE_DAYS, FILTER_PROFILES and OUTDOORS_VARIANTS now live in ./config.js.)
// Current active filter profile
@@ -510,6 +626,24 @@ export function UTCIForecast() {
glob=${currentRow?.glob ?? 0}
activeEvent=${lensEvent}
/>
{/* ── FUTURE FEATURE v2: Historical Context Line ─────────────────
Show a subtle single line directly below the dial readout:
e.g. "3.2°C above the May average for this location"
"Near normal for late August"
Design notes:
• Must be visually elegant — same Fraunces/Manrope type pairing
as the rest of the dial area, small and muted
• Positive delta: warm amber tone; negative delta: cool blue tone
• Source: Open-Meteo has a free /climate endpoint that returns
monthly climate normals (ERA5 reanalysis) for any lat/lon.
Fetch once on location change, cache in a ref. Compare today's
peak air temp (or UTCI) to that month's normal.
• Could also show min/max historical context for the week:
"Warmest day forecast this week" / "Coolest night since March"
• The fetch is separate from the main forecast — handle its own
loading/error state independently so it doesn't block the UI.
─────────────────────────────────────────────────────────────── */}
</div>
<div class="header-right">
@@ -1117,6 +1251,54 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html`<button cla
show, remove the visibleCols check around it. To rename a
heading, edit the text inside the matching <th>.
-->
{/* ── FUTURE FEATURE v2: Sticky "Now" Row + Resizable Table ────────
TWO related ideas for the table UX:
1. AUTO-SCROLL TO NOW ON LOAD
On first render (or day change to today), scroll the tbody
so the current-hour row is visible — ideally centred or at
the top third of the viewport. Use a ref on the now-row <tr>
and call scrollIntoView({ block: 'center', behavior: 'smooth' })
inside a useEffect that fires when days[selectedDay] changes.
The now row already has `currentRow` identified — just need
to attach a ref and fire the scroll.
2. VERTICALLY RESIZABLE TABLE (extends existing drag-to-scroll)
The table already has horizontal drag-to-scroll (useTableScroll).
For v2, make the tbody height resizable — a drag handle on the
bottom edge of the table, similar to how devtools panels resize.
Store the preferred height in localStorage ('sunscope_table_height').
Combined with the now-row auto-scroll, the user sets the table to
exactly the number of rows they want to see at once and it always
opens at the current hour. Very clean UX.
────────────────────────────────────────────────────────────────── */}
{/* ── FUTURE FEATURE v2: Mobile Card Layout ─────────────────────────
On small screens the wide table is painful. Consider a responsive
breakpoint (e.g. < 640px) that switches from the table to a
vertical stack of hour cards:
┌──────────────────────────────────┐
│ 14:00 🌤️ SkyScope porthole │
│ Air 24°C Wind 12 km/h SW │
│ UTCI+P ████░░░░ 28.4°C Warm │
│ UV 6 · Burn 35 min · Precip
└──────────────────────────────────┘
Each card shows only the columns that are active in the current
profile — same visibility logic, different layout. The card design
should stay true to the brass/parchment aesthetic.
Implementation approach:
• CSS media query switches .utci-table-wrap to display:none
and shows a .utci-card-list instead (same data, different markup)
• OR: render the cards in JS from the same hourlyRows array,
conditionally based on a useWindowWidth() hook
• Needs careful thought on which columns to show in "summary"
view vs an expandable "detail" tap — don't want to overwhelm
the card but also don't want to hide too much.
• General spacing and layout passes needed for mobile regardless
of whether the card view is implemented in v2.
────────────────────────────────────────────────────────────────── */}
<div class=${`utci-table-wrap${tableCanScrollLeft ? ' scroll-fade-left' : ''}${tableCanScrollRight ? ' scroll-fade-right' : ''}`} ref=${tableWrapRef}>
<span class="utci-scroll-chevron left" aria-hidden="true"></span>
<span class="utci-scroll-chevron right" aria-hidden="true"></span>
+80
View File
@@ -81,6 +81,46 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
const mugwortPollen= getAq('mugwort_pollen', iso);
const olivePollen = getAq('olive_pollen', iso);
const ragweedPollen= getAq('ragweed_pollen', iso);
// ── FUTURE FEATURE: Activity "What If" Modifier ───────────────────────────
// Add two extra columns driven by a user-selected activity level. These are
// intentionally kept SEPARATE from the core columns above so that baseline
// profile data stays consistent and comparable across profiles.
//
// The user picks an activity from a simple UI picker (no live data needed —
// this is a forecast/planning tool, not a tracker):
// Resting → Walking → Cycling → Running → Sport/Intense
//
// Two output columns only (keep it clean):
//
// adjustedSafeTime — baseline UV safe exposure time × an activity multiplier.
// Higher activity = shorter safe time, because:
// • metabolic heat raises core body temp
// • sweating washes away sunscreen faster
// • more skin blood flow = higher UV sensitivity
// Suggested multipliers (tune with real data):
// Resting: 1.0 (no change)
// Walking: 0.85
// Cycling: 0.75
// Running: 0.60
// Sport: 0.50
//
// heatStressLevel — a simple label: 'Low' | 'Moderate' | 'High' | 'Very High'
// Derived from UTCI + activity heat load. A runner at
// UTCI 28°C should read 'High' even if a resting person
// would read 'Moderate' at the same UTCI.
// Colour code in the UI: 🟢 🟡 🟠 🔴
//
// Implementation sketch:
// 1. Accept `activityLevel` as a new param to buildHourlyRows() alongside
// vehicleType, buildingType etc.
// 2. Define ACTIVITY_PRESETS in utils.js (multiplier + utciOffset per level).
// 3. Compute adjustedSafeTime = baseSafeTime * preset.multiplier
// 4. Compute heatStressLevel from (utci + preset.utciOffset) banded into labels.
// 5. Add both fields to the returned row object below.
// 6. In components.js, render these as optional columns that only appear when
// an activity other than 'Resting' is selected — keeps the default table clean.
// ─────────────────────────────────────────────────────────────────────────────
return {
iso, dt, Ta, RH, dew, va, wd, gust, dir, dif, glob,
cc, ccLow, ccMid, ccHigh, cloudCat,
@@ -114,5 +154,45 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
day.rows.push(row);
});
// ── FUTURE FEATURE v2: Today Summary + Alert System ──────────────────────────
// After grouping rows into days, generate a per-day summary object that powers
// a stylish "Today at a Glance" panel shown above or below the main dial.
//
// The summary is NOT a live alert/push system — it's a forecast digest that
// refreshes with the forecast data. Think of it as a smart briefing card.
//
// WHAT TO COMPUTE (per day, from that day's rows):
// • Peak UTCI+P and time it occurs → heat stress headline
// • Min UTCI+P and time → cold stress headline
// • Peak UV index and time → UV warning
// • Max precipitation rate and time → rain/ice warning
// • Max vehicle cabin temp → "dangerous to leave pets/children in car"
// • Max pollen level + type → pollen advisory
// • Road condition risk (low air temp + precip → ice risk)
//
// ALERT CATEGORIES (each generates a styled warning card if threshold exceeded):
// 🌡️ Heat warning UTCI+P > 32°C
// 🥶 Cold warning UTCI+P < 0°C
// ☀️ UV warning UV index > 6
// 🌧️ Heavy rain precip > 4mm/h
// 🧊 Ice/road risk Ta < 3°C + any precip (or recent precip overnight)
// 🚗 Vehicle danger vehicleT > 35°C ("don't leave pets or children in car")
// 🌿 High pollen any pollen type > 50 grains/m³
//
// DESIGN NOTES:
// • Cards should be concise — one line of bold text + a short explanation
// • Colour-coded to match the existing UTCI stress band palette
// • Collapsible — show top 2-3 alerts by default, expand for full list
// • For today only (days[0]); optionally extend to day tabs in a later pass
// • The "X°C above seasonal norm" historical context line (see app.js comment)
// could live here too, as a subtle subheading under the dial temperature
//
// Suggested return shape — add to the return value below:
// daySummaries: days.map(day => buildDaySummary(day.rows))
//
// where buildDaySummary() is a new helper in this file (or a separate
// summary.js module if it grows large).
// ─────────────────────────────────────────────────────────────────────────────
return { hourlyRows, days, utcOffsetMs };
}
+27
View File
@@ -61,6 +61,33 @@ export const FILTER_PROFILES = {
icon: '⚙️',
proOnly: true,
cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: false, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: false },
// ── FUTURE FEATURE v2: Saved Custom Profiles (Pro) ───────────────────────
// Expand 'Custom' into a fully user-owned profile system. Pro users can:
//
// • CREATE named profiles — give them a name and icon, choose any columns
// • SAVE multiple profiles — stored in localStorage (or user account if we
// add auth), each appearing as an extra pill in the profile button bar
// • REORDER columns — drag-and-drop within the custom profile editor.
// Column order is just an array of keys; shuffle the array, re-render.
// The existing col visibility flags stay, reordering is a separate
// `colOrder: ['hour', 'air', 'wind', ...]` array on the profile object.
// • DELETE / RENAME profiles in a small management modal
//
// The 'Custom' button becomes " New Profile" when in profile-creation mode.
// Existing preset profiles (Basic, Home, Vehicle…) remain read-only.
//
// Data shape per saved profile:
// {
// id: 'profile_abc123', // generated uuid
// label: 'My Beach Days',
// icon: '🏖️', // user picks from a small emoji set
// cols: { ...standard col flags },
// colOrder: ['hour', 'air', 'burn', 'wind', ...], // user's preferred order
// createdAt: ISO string,
// }
//
// Storage: JSON.stringify array in localStorage under 'sunscope_custom_profiles'.
// ─────────────────────────────────────────────────────────────────────────────
},
};
+52
View File
@@ -292,6 +292,41 @@ export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'c
return Math.max(Ta, Math.min(Ti, 90));
}
// ── FUTURE FEATURE v2: Pets Profile — Fur Temperature & Heat Stress ──────────
// Dogs and cats experience heat very differently from humans:
//
// FUR SURFACE TEMPERATURE
// Dark/thick fur absorbs solar radiation and can run significantly hotter
// than air temperature — similar in principle to calcConcreteTemp() but
// with fur-specific albedo values:
// Black fur: albedo ~0.05 (almost all radiation absorbed)
// Brown fur: albedo ~0.15
// Golden fur: albedo ~0.25
// White fur: albedo ~0.40
// Add a calcFurSurfaceTemp(Ta, globalRad, windSpeed, furAlbedo) function
// mirroring calcConcreteTemp but with appropriate convective coefficients
// for fur (lower hc than concrete — fur insulates).
//
// PAW BURN RISK
// Paw pads are highly sensitive to surface temperature. Use calcConcreteTemp()
// output directly — the existing concrete surface temp is already the right
// number. Threshold guidance:
// < 40 °C — safe
// 4052 °C — discomfort / possible burn (hold-your-hand-for-7-seconds test)
// > 52 °C — burns within 60 seconds
// Display as a simple traffic-light column in the Pets profile.
//
// BREED-SPECIFIC HEAT STRESS
// Brachycephalic breeds (Bulldogs, Pugs, French Bulldogs, Persians) have
// severely impaired thermoregulation — their safe UTCI ceiling is much lower.
// A breed selector (Normal / Brachycephalic / Senior) applies a risk multiplier
// to the UTCI thresholds, similar to the planned activity modifier.
//
// SUGGESTED COLUMNS FOR PETS PROFILE
// Hour | Air Temp | UTCI+P | Fur Surface Temp | Paw Burn Risk | Shade Advised
//
// ─────────────────────────────────────────────────────────────────────────────
// ── FUTURE FEATURE: Vehicle-at-speed thermal model ────────────────────────────
// Idea: extend calcVehicleInteriorTemp (or add a companion function) to model
// cabin temperature for a vehicle travelling at speed, not just parked.
@@ -314,6 +349,23 @@ export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'c
//
// This would be a useful companion output column (e.g. "Vehicle (moving)")
// for road-trip planning, dog-in-car safety at a rest stop vs. motorway, etc.
//
// ── CYCLIST AT SPEED (companion to the above) ────────────────────────────────
// A cyclist generates their own headwind, so the felt temperature (UTCI) is
// very different from a stationary person — even without ambient wind.
// Could share the same speed-class approach as the vehicle model:
// Slow (10 mph) / Moderate (15 mph) / Fast (25 mph)
//
// Key differences from the vehicle model:
// • The cyclist IS the exposed person — use the UTCI polynomial directly
// with va = max(ambientWind, cyclingSpeed * conversionFactor)
// • No cabin heating effect — the cyclist gets wind chill, not solar entrapment
// • High metabolic heat generation raises core temp (links to the activity
// modifier planned in compute.js — cycling at speed is a combined effect)
// • UVA/UVB exposure is unchanged — still fully exposed to the sun
//
// Could be a sub-option within the existing Cycling activity variant rather
// than a separate column — e.g. a speed picker in the variant controls.
// ─────────────────────────────────────────────────────────────────────────────
// ═══════════════════════════════════════════════════════════════════