Refactor widgets to use theme better. Alert change and 7day vaa

This commit is contained in:
2026-10-20 14:05:52 -07:00
parent bd5e02ac1b
commit c027a30b3f
3 changed files with 83 additions and 83 deletions
+38 -37
View File
@@ -60,38 +60,38 @@ export default function AlertChanges({ settings, widgetID}) {
return sorted_entries; return sorted_entries;
}, [apiData, thisWidget]); }, [apiData, thisWidget]);
// list of alert colors used for coloring text below // Alert colors are now light/dark mode responsive
const alertColors = { const alertColors = {
Red: "red.400", Red: "alertRed",
Orange: "orange.400", Orange: "alertOrange",
Yellow: "yellow.400", Yellow: "alertYellow",
Green: "green.400", Green: "alertGreen",
}; };
if (isLoading) return ( if (isLoading) return (
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="gray.800"> <Flex w="100%" h="100%" align="center" justify="center" bg="transparent">
<Spinner size="xl" color="blue.400" thickness="4px"/> <Spinner size="xl" color="blue.400" thickness="4px"/>
</Flex> </Flex>
); );
if (error) return ( if (error) return (
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="gray.800"> <Flex w="100%" h="100%" align="center" justify="center" bg="transparent">
<Text color="red.400">{error}</Text> <Text color="red.400">{error}</Text>
</Flex> </Flex>
); );
if (data.length === 0) return ( if (data.length === 0) return (
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="gray.800"> <Flex w="100%" h="100%" align="center" justify="center" bg="transparent">
<Text textAlign="center" color="gray.400">No flight level data available for the selected dates.</Text> <Text textAlign="center" color="widgetTextMuted">No flight level data available for the selected dates.</Text>
</Flex> </Flex>
); );
return ( return (
<Box display="flex" flexDirection="column" gap={2} p={2} w="100%" h="100%" borderRadius="lg" bg="gray.800" overflowY="auto"> // bg="transparent" to let the glass panel show through
<Box display="flex" flexDirection="column" gap={2} p={2} w="100%" h="100%" bg="transparent" overflowY="auto">
{data.map((volcano, index) => { {data.map((volcano, index) => {
if (thisWidget?.size === "large") return ( if (thisWidget?.size === "large") return (
<Collapsible.Root key={index} w="100%"> <Collapsible.Root key={index} w="100%">
{/* Using context to dynamically change the border radii of the triggers */}
<Collapsible.Context> <Collapsible.Context>
{(collapsible) => ( {(collapsible) => (
<> <>
@@ -102,57 +102,58 @@ export default function AlertChanges({ settings, widgetID}) {
p={1} pl={3} pr={3} p={1} pl={3} pr={3}
borderTopRadius="lg" borderTopRadius="lg"
borderBottomRadius={collapsible.open ? "0" : "lg"} borderBottomRadius={collapsible.open ? "0" : "lg"}
bg="gray.600" bg="widgetItemBg"
> >
<Flex direction="column" w="100%" minW={0} textAlign="left" color="gray.100"> <Flex direction="column" w="100%" minW={0} textAlign="left">
<Text truncate >{volcano[0]}</Text> {/* volcano name */} <Text truncate color="text" fontWeight="medium">{volcano[0]}</Text>
<Text truncate fontSize="xs" color="gray.300/90">{volcano[1][0]?.alert_start_date_local}</Text> <Text truncate fontSize="xs" color="widgetTextMuted">{volcano[1][0]?.alert_start_date_local}</Text>
</Flex> </Flex>
<Flex w="100%" minW={0} justify="right"> <Flex w="100%" minW={0} justify="right">
<Text truncate pl={2} pr={2} borderRadius="lg" bg="gray.800/50" color="gray.100" fontWeight="semibold">{volcano[1][0]?.alert_level_name}</Text> <Text truncate pl={2} pr={2} borderRadius="lg" bg="widgetBadgeBg" color="text" fontWeight="semibold">
{volcano[1][0]?.alert_level_name}
</Text>
</Flex> </Flex>
</Flex> </Flex>
</Collapsible.Trigger> </Collapsible.Trigger>
<Collapsible.Content w="100%" p={3} borderBottomRadius="lg" bg="gray.700">
<Collapsible.Content w="100%" p={3} borderBottomRadius="lg" bg="widgetItemExpandedBg">
<Timeline.Root size="md" variant="subtle"> <Timeline.Root size="md" variant="subtle">
{volcano[1]?.map((object, index) => ( {volcano[1]?.map((object, index) => (
<Timeline.Item key={index} w="100%" h="100%"> <Timeline.Item key={index} w="100%" h="100%">
<Timeline.Connector > <Timeline.Connector >
{/*<Timeline.Separator borderWidth="1px" borderColor="gray.600"/>*/} <Timeline.Indicator outline="none" bg="widgetBadgeBg" color="iconColor" size="lg">
<Timeline.Indicator outline="none" bg="gray.200" color="gray.500" size="lg">
{object?.alert_scheme_domain === "terrestrial" && <Footprints size={15} />} {object?.alert_scheme_domain === "terrestrial" && <Footprints size={15} />}
{object?.alert_scheme_domain === "aviation" && <Plane size={15} />} {object?.alert_scheme_domain === "aviation" && <Plane size={15} />}
{object?.alert_scheme_domain === "maritime" && <Sailboat size={15} />} {object?.alert_scheme_domain === "maritime" && <Sailboat size={15} />}
</Timeline.Indicator> </Timeline.Indicator>
<Timeline.Indicator h="50%" outline="none" bg="gray.700" color="gray.100" size="lg"> <Timeline.Indicator h="50%" outline="none" bg="widgetBadgeBg" color="iconColor" size="lg">
{object?.change_type === "increase" ? <TrendingUp /> : <TrendingDown />} {object?.change_type === "increase" ? <TrendingUp /> : <TrendingDown />}
</Timeline.Indicator> </Timeline.Indicator>
</Timeline.Connector> </Timeline.Connector>
<Timeline.Content w="100%" h="100%"> <Timeline.Content w="100%" h="100%">
{/*<Timeline.Title>{object?.volcano}</Timeline.Title>*/}
<Timeline.Description w="100%" h="100%" gap={1}> <Timeline.Description w="100%" h="100%" gap={1}>
<Flex direction="column" gap={1}> <Flex direction="column" gap={1}>
<Text w="100%" h="100%" color="gray.200">{object?.alert_start_date_local}</Text> <Text w="100%" h="100%" color="widgetTextMuted">{object?.alert_start_date_local}</Text>
<Text w="100%" h="100%" <Text w="100%" h="100%"
color={alertColors[object?.alert_level_name] || "gray.200"} color={alertColors[object?.alert_level_name] || "text"}
fontSize="md" fontSize="md"
fontWeight="semibold" fontWeight="semibold"
> >
{object?.alert_level_name} {object?.alert_level_name}
</Text> </Text>
<Flex w="100%" h="20px" direction="row" justify="left" align="center"> <Flex w="100%" h="20px" direction="row" justify="left" align="center">
<Text color="gray.300">Alert Scale: </Text> <Text color="widgetTextMuted">Alert Scale: </Text>
{(Array.from({ length: Number(object?.alert_scheme_highest) - Number(object?.alert_scheme_lowest) + 1}, (v, i) => Number(object?.alert_scheme_lowest) + i)) {(Array.from({ length: Number(object?.alert_scheme_highest) - Number(object?.alert_scheme_lowest) + 1}, (v, i) => Number(object?.alert_scheme_lowest) + i))
.map((num) => ( .map((num) => (
<Center <Center
key={num} key={num}
w="20px" h="100%" w="20px" h="100%"
color="gray.300" color="text"
bg={num === Number(object?.alert_level_rank) ? "gray.300/20" : null} // alert level rank is a string for some reason bg={num === Number(object?.alert_level_rank) ? "widgetBadgeBg" : "transparent"}
borderRadius={num === Number(object?.alert_level_rank) ? "xl" : null} borderRadius={num === Number(object?.alert_level_rank) ? "xl" : "none"}
textAlign="center" textAlign="center"
fontSize="sm" fontSize="sm"
fontWeight="semibold" fontWeight="bold"
> >
{num} {num}
</Center> </Center>
@@ -181,30 +182,30 @@ export default function AlertChanges({ settings, widgetID}) {
justify="space-between" align="center" justify="space-between" align="center"
p={1} pl={3} pr={3} p={1} pl={3} pr={3}
borderRadius="lg" borderRadius="lg"
bg="gray.600" bg="widgetItemBg"
> >
<Flex direction="column" w="100%" minW={0} textAlign="left" color="gray.100"> <Flex direction="column" w="100%" minW={0} textAlign="left" color="text">
<Flex direction="row" w="100%" gap={2} align="center"> <Flex direction="row" w="100%" gap={2} align="center">
<Flex color="gray.300"> <Flex color="iconColor">
{domain[0] === "aviation" && <Plane size={15}/>} {domain[0] === "aviation" && <Plane size={15}/>}
{domain[0] === "terrestrial" && <Footprints size={15}/>} {domain[0] === "terrestrial" && <Footprints size={15}/>}
{domain[0] === "maritime" && <Sailboat size={15}/>} {domain[0] === "maritime" && <Sailboat size={15}/>}
</Flex> </Flex>
<Text truncate>{volcano[0] || ""}</Text> {/* volcano name */} <Text truncate fontWeight="medium">{volcano[0] || ""}</Text>
</Flex> </Flex>
<Text truncate w="100%" fontSize="xs" color="gray.300/90">{domain[1]?.[0]?.alert_start_date_local || ""}</Text> <Text truncate w="100%" fontSize="xs" color="widgetTextMuted">{domain[1]?.[0]?.alert_start_date_local || ""}</Text>
</Flex> </Flex>
<Flex direction="row" w="100%" minW={0} justify="right" gap={2} color="gray.200"> <Flex direction="row" w="100%" minW={0} justify="right" gap={2} color="text">
<Flex w="25px" justify="right"> <Flex w="25px" justify="right" color="iconColor">
{domain[1]?.[0]?.change_type === "increase" ? <TrendingUp/> : <TrendingDown/>} {domain[1]?.[0]?.change_type === "increase" ? <TrendingUp/> : <TrendingDown/>}
</Flex> </Flex>
<Text <Text
truncate truncate
pl={2} pr={2} pl={2} pr={2}
borderRadius="lg" borderRadius="lg"
bg="gray.800/50" bg="widgetBadgeBg"
color={alertColors[domain[1]?.[0]?.alert_level_name] || "gray.100"} color={alertColors[domain[1]?.[0]?.alert_level_name] || "text"}
fontWeight="semibold" fontWeight="semibold"
> >
{domain[1]?.[0]?.alert_level_name} {domain[1]?.[0]?.alert_level_name}
@@ -23,7 +23,6 @@ export default function VaaActivity7Day({ settings, widgetID }) {
'hexagon', 'octagon', 'cross', 'x' 'hexagon', 'octagon', 'cross', 'x'
]; ];
// Sort data descending by flight level (matching Observable behavior)
const sortedData = [...apiData].sort((a, b) => b.obs_fl - a.obs_fl); const sortedData = [...apiData].sort((a, b) => b.obs_fl - a.obs_fl);
// Efficiently group into Plotly traces by volcano // Efficiently group into Plotly traces by volcano
@@ -44,102 +43,90 @@ export default function VaaActivity7Day({ settings, widgetID }) {
mode: 'markers', mode: 'markers',
marker: { marker: {
size: 7, size: 7,
line: { width: .5, color: 'rgba(255, 255, 255, 0.7)' }, line: { width: 0.5, color: 'var(--chakra-colors-textInverted)' },
symbol: Symbolway[index % Symbolway.length], symbol: Symbolway[index % Symbolway.length],
}, },
// Replicating the clean Observable tooltip // Replicating the clean Observable tooltip
hovertemplate: hovertemplate:
`<b>${volcanoName}</b><br>` + `<b>${volcanoName}</b><br>` +
'Date: %{x|%b %d %Y %H:%M}<br>' + 'Date: %{x|%b %d %Y %H:%M}<br>' +
'Flight Level: %{y}<extra></extra>' 'Flight Level: %{y}<extra></extra>'
})); }));
}, [apiData]); }, [apiData]);
// 2. Build the Layout (Styling and Grid) // 2. Build the Layout (Styling and Grid)
const layout = useMemo(() => { const layout = useMemo(() => {
return { return {
autosize: true, autosize: true,
paper_bgcolor: 'transparent', // Let Chakra UI background show through paper_bgcolor: 'transparent', // Let Chakra UI background show through
plot_bgcolor: 'transparent', plot_bgcolor: 'transparent',
font: { color: '#d1d5db' }, // gray.300 font: { color: 'var(--chakra-colors-widgetTextMuted)' },
margin: { t: 40, r: 20, l: 20, b: 20 }, margin: { t: 40, r: 20, l: 20, b: 20 },
// title: {
// text: '7 Day Consolidated Flight Levels',
// font: { size: 16, color: 'white' },
// x: 0.05
// },
xaxis: { xaxis: {
title: { title: {
text: 'Date (UTC)', text: 'Date (UTC)',
font: { color: 'white', size: 12 } font: { color: 'var(--chakra-colors-text)', size: 12 }
}, },
type: 'date', type: 'date',
tickformat: '%b %d<br>%Y %H:%M', // Multiline ticks from Observable tickformat: '%b %d<br>%Y %H:%M',
gridcolor: '#4b5563', gridcolor: 'var(--chakra-colors-glassSeperator)',
zerolinecolor: '#4b5563', zerolinecolor: 'var(--chakra-colors-glassSeperator)',
automargin: true, automargin: true,
}, },
yaxis: { yaxis: {
title: { title: {
text: 'Flight Level', text: 'Flight Level',
font: { color: 'white', size: 12 } font: { color: 'var(--chakra-colors-text)', size: 12 }
}, },
type: plotYAxisType, type: plotYAxisType,
gridcolor: '#4b5563', gridcolor: 'var(--chakra-colors-glassSeperator)',
zerolinecolor: '#ffffff', zerolinecolor: 'var(--chakra-colors-text)',
automargin: true, automargin: true,
}, },
// legend: {
// orientation: 'h',
// y: -0.2,
// x: 0.5,
// xanchor: 'center'
// },
hovermode: 'closest', hovermode: 'closest',
shapes: [ shapes: [
{ {
type: 'line', type: 'line',
x0: 0, x0: 0,
x1: 1, x1: 1,
xref: 'paper', // Spans entire chart width xref: 'paper',
y0: 0, y0: 0,
y1: 0, y1: 0,
yref: 'y', yref: 'y',
line: { color: 'white', width: 1 } line: { color: 'var(--chakra-colors-text)', width: 1 }
} }
], ],
}; };
}, [apiData, plotYAxisType]); }, [apiData, plotYAxisType]);
if (isLoading) return ( if (isLoading) return (
<Flex w="100%" h="100%" bg="gray.800" borderRadius="md" align="center" justify="center"> <Flex w="100%" h="100%" bg="transparent" align="center" justify="center">
<Spinner size="xl" color="blue.400" thickness="4px" /> <Spinner size="xl" color="blue.400" thickness="4px" />
</Flex> </Flex>
); );
if (error) return ( if (error) return (
<Flex w="100%" h="100%" bg="gray.800" borderRadius="md" align="center" justify="center" p={4}> <Flex w="100%" h="100%" bg="transparent" align="center" justify="center" p={4}>
<Text color="red.400">{error}</Text> <Text color="red.400">{error}</Text>
</Flex> </Flex>
); );
if (plotTraces.length === 0) return ( if (plotTraces.length === 0) return (
<Flex height="100%" width="100%" align="center" justify="center" bg="gray.800" borderRadius="md"> <Flex height="100%" width="100%" align="center" justify="center" bg="transparent">
<Text color="gray.400">No flight level data available.</Text> <Text color="widgetTextMuted">No flight level data available.</Text>
</Flex> </Flex>
); );
return ( return (
<Box height="100%" width="100%" bg="gray.800" borderRadius="md" p={2} overflow="hidden"> <Box height="100%" width="100%" bg="transparent" overflow="hidden">
<Plot <Plot
key={plotYAxisType} key={plotYAxisType}
data={plotTraces} data={plotTraces}
layout={layout} layout={layout}
useResizeHandler={true} // Essential for dashboard grid resizing! useResizeHandler={true}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
config={{ responsive: true, displayModeBar: true, displaylogo: false }} config={{ responsive: true, displayModeBar: true, displaylogo: false }}
/> />
</Box> </Box>
); );
} }
+12
View File
@@ -572,6 +572,18 @@ const config = defineConfig({
iconColor: { value: { base: "gray.600", _dark: "gray.300" } }, // Fixed: Visible in dark mode iconColor: { value: { base: "gray.600", _dark: "gray.300" } }, // Fixed: Visible in dark mode
glassSeperator: { value: { base: "#0000001A", _dark: "#FFFFFF3F" } }, glassSeperator: { value: { base: "#0000001A", _dark: "#FFFFFF3F" } },
gooeyButtonBackground: {value: {base: "rgba(255, 255, 255, 0.1)", _dark: "rgba(255, 255, 255, 0.1)"}}, gooeyButtonBackground: {value: {base: "rgba(255, 255, 255, 0.1)", _dark: "rgba(255, 255, 255, 0.1)"}},
// WIDGET SPECIFIC VALUES
widgetItemBg: { value: { base: "rgba(0, 0, 0, 0.04)", _dark: "rgba(255, 255, 255, 0.05)" } },
widgetItemExpandedBg: { value: { base: "rgba(0, 0, 0, 0.08)", _dark: "rgba(0, 0, 0, 0.3)" } },
widgetBadgeBg: { value: { base: "rgba(255, 255, 255, 0.6)", _dark: "rgba(0, 0, 0, 0.5)" } },
widgetTextMuted: { value: { base: "gray.600", _dark: "gray.400" } },
// ALERT COLORS
alertRed: { value: { base: "red.600", _dark: "red.400" } },
alertOrange: { value: { base: "orange.600", _dark: "orange.400" } },
alertYellow: { value: { base: "yellow.600", _dark: "yellow.400" } },
alertGreen: { value: { base: "green.600", _dark: "green.400" } },
}, },
}, },
tokens: { tokens: {