diff --git a/src/components/AIPollutionForecast.jsx b/src/components/AIPollutionForecast.jsx index bce57ab..41b47c1 100644 --- a/src/components/AIPollutionForecast.jsx +++ b/src/components/AIPollutionForecast.jsx @@ -1,4 +1,5 @@ import React, { useState, useMemo, useCallback } from "react"; +import { SelectionButton } from "./ui/PressableCard"; // ─── Forecast Data Generator ──────────────────────────────────────────────── const POLLUTANTS = [ @@ -1040,13 +1041,19 @@ export default function AIPollutionForecast() {

{WEATHER_CONDITIONS.map((w, i) => ( -
setSelectedWeather(selectedWeather === i ? null : i)}> + setSelectedWeather(selectedWeather === i ? null : i)} + label={`Weather condition: ${w.label}, dispersal ${w.dispersal.toFixed(1)}x`} + style={styles.weatherTile(selectedWeather === i)} + >
{w.icon}
{w.label}
1 ? "#22c55e" : w.dispersal > 0.5 ? "#eab308" : "#ef4444", marginTop: "4px" }}> Dispersal: {w.dispersal.toFixed(1)}x
-
+ ))}
diff --git a/src/components/HealthImpactDashboard.jsx b/src/components/HealthImpactDashboard.jsx index a803f9f..97c6f8e 100644 --- a/src/components/HealthImpactDashboard.jsx +++ b/src/components/HealthImpactDashboard.jsx @@ -1,4 +1,5 @@ -import React, { useState, useMemo, useCallback } from "react"; +import React, { useCallback, useId, useMemo, useState } from "react"; +import { DisclosureButton } from "./ui/PressableCard"; // ─── AQI & Health Data ────────────────────────────────────────────────────── const AQI_CATEGORIES = [ @@ -147,7 +148,11 @@ function AQIGauge({ aqi }) { } function HealthScoreRing({ score }) { - const circumference = 2 * Math.PI(40); + // Math.PI(40) -- `Math.PI` is a number, so calling it threw + // "TypeError: Math.PI is not a function" and took the whole dashboard down on render. + // Fixed here because the keyboard fix below cannot be verified on a component that + // cannot mount. r=40 matches the this dash array is drawn onto. + const circumference = 2 * Math.PI * 40; const offset = circumference - (score / 100) * circumference; const color = score >= 80 ? "#22c55e" : score >= 60 ? "#eab308" : score >= 40 ? "#f97316" : "#ef4444"; @@ -182,13 +187,22 @@ function StatCard({ icon, label, value, color, subtext }) { function PollutantCard({ pollutant, aqi, hours }) { const [expanded, setExpanded] = useState(false); + const panelId = useId(); const risk = calculateExposureRisk(aqi, hours, "outdoor"); const riskColor = risk > 70 ? "#ef4444" : risk > 40 ? "#f97316" : risk > 20 ? "#eab308" : "#22c55e"; return ( -
setExpanded(!expanded)}> -
+
+ {/* The header is the control; the panel it opens sits beside it rather than inside + it, so the button's accessible name stays the pollutant rather than becoming the + whole card's text once expanded. */} + setExpanded(!expanded)} + controls={panelId} + label={`${pollutant.pollutant} health effects, exposure risk ${risk}%`} + className="flex items-center justify-between" + >

{pollutant.pollutant}

Exposure risk: {risk}%

@@ -196,10 +210,10 @@ function PollutantCard({ pollutant, aqi, hours }) {
-
+ {expanded && ( -
+

Short-term Effects

    diff --git a/src/components/HealthRiskCards.jsx b/src/components/HealthRiskCards.jsx index 5ff7468..4ab0527 100644 --- a/src/components/HealthRiskCards.jsx +++ b/src/components/HealthRiskCards.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useId, useState } from 'react'; import { motion } from 'framer-motion'; import { TrendingUp, @@ -27,6 +27,7 @@ import { getAQIBand, getRiskScore, } from './healthRiskTypes'; +import { DisclosureButton } from './ui/PressableCard'; /** * Stat card with icon, value, and optional trend. @@ -228,6 +229,7 @@ const VulnerableGroupCard = ({ group, delay = 0 }) => { */ const PollutantEffectCard = ({ pollutant, delay = 0 }) => { const [expanded, setExpanded] = useState(false); + const panelId = useId(); const riskColor = pollutant.riskLevel === 'high' ? '#ef4444' : pollutant.riskLevel === 'moderate' ? '#f59e0b' : '#22c55e'; return ( @@ -243,7 +245,13 @@ const PollutantEffectCard = ({ pollutant, delay = 0 }) => { borderLeft: `3px solid ${riskColor}`, }} > -
    setExpanded(!expanded)}> + setExpanded(!expanded)} + controls={panelId} + label={`${pollutant.label} health risks, ${pollutant.riskLevel} risk`} + style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }} + >

    {pollutant.label}

    @@ -256,10 +264,11 @@ const PollutantEffectCard = ({ pollutant, delay = 0 }) => { }}> {pollutant.riskLevel.toUpperCase()} - -

    +
    {expanded && ( { const cat = pH_SCALE.find(c => r.avgPh >= c.min && c.max > r.avgPh) || pH_SCALE[4]; return ( -
    setSelectedRegion(i)}> - {i === selectedRegion ? "🔵" : "⚪"} + setSelectedRegion(i)} + label={`Ocean region: ${r.name}, average pH ${r.avgPh}`} + style={{ ...s.regionRow, borderColor: selectedRegion === i ? cat.color : "#1e3a5f", background: selectedRegion === i ? `${cat.color}10` : undefined }} + > +
    {r.name}
    {r.lat > 0 ? `${r.lat}°N` : `${Math.abs(r.lat)}°S`} · {Math.abs(r.lon)}°{r.lon > 0 ? "E" : "W"}
    @@ -370,7 +376,7 @@ export default function OceanAcidificationMonitor() {
    {r.avgPh}
    {r.trend}/yr
    -
    + ); })}
    @@ -615,8 +621,13 @@ export default function OceanAcidificationMonitor() { {/* Scenario details */}
    {CO2_SCENARIOS.map(sc => ( -
    setSelectedScenario(sc.id)}> + setSelectedScenario(sc.id)} + label={`CO2 scenario: ${sc.name}, peaking at ${sc.peakPpm} ppm in ${sc.peakYear}`} + style={{ ...s.insightCard, borderLeft: `4px solid ${sc.color}`, cursor: "pointer", opacity: selectedScenario === sc.id ? 1 : 0.6 }} + >
    {sc.name} {selectedScenario === sc.id && Active} @@ -625,7 +636,7 @@ export default function OceanAcidificationMonitor() { Peak: {sc.peakPpm} ppm ({sc.peakYear}) End: {sc.endPpm} ppm
    -
    + ))}
diff --git a/src/components/ReportCards.jsx b/src/components/ReportCards.jsx index 18c080d..4708cd1 100644 --- a/src/components/ReportCards.jsx +++ b/src/components/ReportCards.jsx @@ -7,10 +7,11 @@ import { TrendingDown, TrendingUp } from 'lucide-react'; -import { useState } from 'react'; +import { useId, useState } from 'react'; import { formatReportTimestamp } from '../utils/localDay'; import { COMPLIANCE_STATUSES, formatCurrency } from './reportTypes'; +import { DisclosureButton } from './ui/PressableCard'; /** * Stat card with icon, value, label, and trend. @@ -123,6 +124,7 @@ const ReportItemCard = ({ report, delay = 0, isSelected, onSelect, timeZone }) = */ const ComplianceCard = ({ item, delay = 0 }) => { const [expanded, setExpanded] = useState(false); + const panelId = useId(); const statusConfig = COMPLIANCE_STATUSES[item.status] || COMPLIANCE_STATUSES.pending_review; return ( @@ -135,8 +137,14 @@ const ComplianceCard = ({ item, delay = 0 }) => { borderRadius: '0.75rem', padding: '0.75rem 1rem', borderLeft: `3px solid ${statusConfig.color}`, }} > -
setExpanded(!expanded)}> - {statusConfig.icon} + setExpanded(!expanded)} + controls={panelId} + label={`${item.label} compliance detail, ${item.percentOfStandard || 'unknown'}% of standard`} + style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }} + > +

{item.label}

@@ -147,10 +155,10 @@ const ComplianceCard = ({ item, delay = 0 }) => { {item.percentOfStandard || '—'}%

of standard

- -
+