diff --git a/client/src/components/Widgets/AlertChanges.jsx b/client/src/components/Widgets/AlertChanges.jsx index 9f6dbd2..42eea0d 100644 --- a/client/src/components/Widgets/AlertChanges.jsx +++ b/client/src/components/Widgets/AlertChanges.jsx @@ -60,38 +60,38 @@ export default function AlertChanges({ settings, widgetID}) { return sorted_entries; }, [apiData, thisWidget]); - // list of alert colors used for coloring text below + // Alert colors are now light/dark mode responsive const alertColors = { - Red: "red.400", - Orange: "orange.400", - Yellow: "yellow.400", - Green: "green.400", + Red: "alertRed", + Orange: "alertOrange", + Yellow: "alertYellow", + Green: "alertGreen", }; if (isLoading) return ( - + ); if (error) return ( - + {error} ); if (data.length === 0) return ( - - No flight level data available for the selected dates. + + No flight level data available for the selected dates. ); return ( - + // bg="transparent" to let the glass panel show through + {data.map((volcano, index) => { if (thisWidget?.size === "large") return ( - {/* Using context to dynamically change the border radii of the triggers */} {(collapsible) => ( <> @@ -102,57 +102,58 @@ export default function AlertChanges({ settings, widgetID}) { p={1} pl={3} pr={3} borderTopRadius="lg" borderBottomRadius={collapsible.open ? "0" : "lg"} - bg="gray.600" + bg="widgetItemBg" > - - {volcano[0]} {/* volcano name */} - {volcano[1][0]?.alert_start_date_local} + + {volcano[0]} + {volcano[1][0]?.alert_start_date_local} - {volcano[1][0]?.alert_level_name} + + {volcano[1][0]?.alert_level_name} + - + + {volcano[1]?.map((object, index) => ( - {/**/} - + {object?.alert_scheme_domain === "terrestrial" && } {object?.alert_scheme_domain === "aviation" && } {object?.alert_scheme_domain === "maritime" && } - + {object?.change_type === "increase" ? : } - {/*{object?.volcano}*/} - {object?.alert_start_date_local} + {object?.alert_start_date_local} {object?.alert_level_name} - Alert Scale: + Alert Scale: {(Array.from({ length: Number(object?.alert_scheme_highest) - Number(object?.alert_scheme_lowest) + 1}, (v, i) => Number(object?.alert_scheme_lowest) + i)) .map((num) => (
{num}
@@ -181,30 +182,30 @@ export default function AlertChanges({ settings, widgetID}) { justify="space-between" align="center" p={1} pl={3} pr={3} borderRadius="lg" - bg="gray.600" + bg="widgetItemBg" > - + - + {domain[0] === "aviation" && } {domain[0] === "terrestrial" && } {domain[0] === "maritime" && } - {volcano[0] || ""} {/* volcano name */} + {volcano[0] || ""} - {domain[1]?.[0]?.alert_start_date_local || ""} + {domain[1]?.[0]?.alert_start_date_local || ""} - - + + {domain[1]?.[0]?.change_type === "increase" ? : } {domain[1]?.[0]?.alert_level_name} diff --git a/client/src/components/Widgets/ExplosionEvents.jsx b/client/src/components/Widgets/ExplosionEvents.jsx index 2ddbf51..0f1e742 100644 --- a/client/src/components/Widgets/ExplosionEvents.jsx +++ b/client/src/components/Widgets/ExplosionEvents.jsx @@ -5,13 +5,17 @@ import Plot from 'react-plotly.js'; import VdapApi from '../../hooks/useVdapApi.js'; import useBuildUrl from "@/hooks/useBuildUrl.js"; +// 1. Import your central color dictionary +import { rawWidgetColors } from '../../theme.js'; + export default function ExplosionEvents({ settings, widgetID }) { // Plotly expects 'linear' for normal axes const plotYAxisType = settings?.yAxisScale === 'log' ? 'log' : 'linear'; // 🎨 Let's check color mode so we can make people happy with light and dark const { colorMode } = useColorMode(); - const isDark = colorMode === 'dark'; + // Assign the active palette based on the current mode + const plotColors = colorMode === 'dark' ? rawWidgetColors.dark : rawWidgetColors.light; // Generate url with input dates, default to 7 day range const url = useBuildUrl('/api/v2/explosion_events', settings?.startDate, settings?.endDate); @@ -53,58 +57,46 @@ export default function ExplosionEvents({ settings, widgetID }) { mode: 'markers', marker: { size: 7, - line: { width: .5, color: 'rgba(255, 255, 255, 0.7)' }, + line: { width: .5, color: plotColors.text }, symbol: Symbolway[index % Symbolway.length], }, // Replicating the clean D3/Observable tooltip hovertemplate: - `${volcanoName}
` + - 'Date (UTC): %{x|%b %d %Y %H:%M}
' + - 'Number of Explosions: %{y}' + `${volcanoName}
` + + 'Date (UTC): %{x|%b %d %Y %H:%M}
' + + 'Number of Explosions: %{y}' })); - }, [apiData]); + }, [apiData, plotColors]); // 2. Build the Layout (Styling and Grid) const layout = useMemo(() => { - return { autosize: true, - paper_bgcolor: 'transparent', // Let Chakra UI background show through + paper_bgcolor: 'transparent', plot_bgcolor: 'transparent', - font: { color: '#d1d5db' }, // gray.300 + font: { color: plotColors.muted }, 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: { title: { text: 'Date (UTC)', - font: { color: 'white', size: 12 } + font: { color: plotColors.text, size: 12 } }, type: 'date', - tickformat: '%b %d
%Y', // D3 requested %b %d \n %Y formatting - gridcolor: '#4b5563', - zerolinecolor: '#4b5563', + tickformat: '%b %d
%Y', + gridcolor: plotColors.grid, + zerolinecolor: plotColors.grid, automargin: true, }, yaxis: { title: { text: 'Number of Explosions', - font: { color: 'white', size: 12 } + font: { color: plotColors.text, size: 12 } }, type: plotYAxisType, - gridcolor: '#4b5563', - zerolinecolor: '#ffffff', + gridcolor: plotColors.grid, + zerolinecolor: plotColors.text, automargin: true, }, - // legend: { - // orientation: 'h', - // y: -0.2, - // x: 0.5, - // xanchor: 'center' - // }, hovermode: 'closest', shapes: [ { @@ -115,40 +107,40 @@ export default function ExplosionEvents({ settings, widgetID }) { y0: 0, y1: 0, yref: 'y', - line: { color: 'white', width: 1 } + line: { color: plotColors.text, width: 1 } } ], }; - }, [apiData, plotYAxisType]); + }, [plotYAxisType, plotColors]); if (isLoading) return ( - - - + + + ); if (error) return ( - - {error} - + + {error} + ); if (plotTraces.length === 0) return ( - - No data available for current filter. - + + No data available for current filter. + ); return ( - - - + + + ); } \ No newline at end of file diff --git a/client/src/components/Widgets/IvansAlerts.jsx b/client/src/components/Widgets/IvansAlerts.jsx index 48451a7..f65db1f 100644 --- a/client/src/components/Widgets/IvansAlerts.jsx +++ b/client/src/components/Widgets/IvansAlerts.jsx @@ -43,30 +43,31 @@ export default function IvansAlerts({ settings, widgetID }) { }).join('\n\n'); navigator.clipboard.writeText(dataText) .then(() => { - alert('Copied to clipboard!'); + alert('Copied to clipboard!'); }) .catch((err => { - console.error('Failed to copy: ', err); + console.error('Failed to copy: ', err); })); updateSettings(widgetID, {copyToClipboard: false}); } }, [settings, some_data, widgetID, updateSettings]); + // Cleaned hardcoded grays for transparent so glass shell shows through if (isLoading) return ( - + ); if (error) return ( - + {error} ); if (some_data.length === 0) return ( - - No flight level data available for the selected dates. + + No flight level data available for the selected dates. ); @@ -78,7 +79,7 @@ export default function IvansAlerts({ settings, widgetID }) { p={2} gap={2} overflow="auto" borderRadius="xl" - bg="gray.800" + bg="transparent" // Let the global widgetRecipe glass shell show through > {some_data.map((item, index) => { if (thisWidget?.size === "large") return ( @@ -96,31 +97,31 @@ export default function IvansAlerts({ settings, widgetID }) { gap={2} borderTopRadius="lg" borderBottomRadius={collapsible.open ? "0" : "lg"} - bg="gray.600" + bg="widgetItemBg" > - + {item.volcano_name} - {item.send_time} + {item.send_time} - {item.report_num} + {item.report_num} {/* This box needs to be at h="100%" for the pdf toolbar to appear */} - - - {item?.summary} - Preparer: {item?.preparer} + + + {item?.summary} + Preparer: {item?.preparer} {item?.files?.map((file, index) => { return (
- {file?.name} - + {file?.name} + Open @@ -131,8 +132,8 @@ export default function IvansAlerts({ settings, widgetID }) { type="application/pdf" style={{ width: '100%', height: '100%', display: 'block' }} > - - + + Your browser cannot display this PDF directly. @@ -151,19 +152,20 @@ export default function IvansAlerts({ settings, widgetID }) { ) else if (thisWidget?.size === "small") return ( - + {item.volcano_name} - {item.send_time} + {item.send_time} - {item.report_num} + {item.report_num} ) diff --git a/client/src/components/Widgets/PlumeEvents.jsx b/client/src/components/Widgets/PlumeEvents.jsx index 925a96d..8b5c6a4 100644 --- a/client/src/components/Widgets/PlumeEvents.jsx +++ b/client/src/components/Widgets/PlumeEvents.jsx @@ -1,10 +1,18 @@ -import VdapApi from '../../hooks/useVdapApi.js'; import { useMemo } from 'react'; +import { Box, Flex, Spinner, Text } from "@chakra-ui/react"; +import { useColorMode } from "@/components/ui/color-mode.jsx"; import Plot from 'react-plotly.js'; -import {Box, Flex, Spinner, Text} from "@chakra-ui/react"; +import VdapApi from '../../hooks/useVdapApi.js'; import useBuildUrl from "@/hooks/useBuildUrl.js"; +// 1. Import your central color dictionary +import { rawWidgetColors } from '../../theme.js'; + export default function PlumeEvents({ settings, widgetID}) { + const { colorMode } = useColorMode(); + // 2. Assign the active palette based on the current mode + const plotColors = colorMode === 'dark' ? rawWidgetColors.dark : rawWidgetColors.light; + // Build url, defaults to 7 days past const url = useBuildUrl('/api/v2/plume_events', settings?.startDate, settings?.endDate); @@ -44,7 +52,7 @@ export default function PlumeEvents({ settings, widgetID}) { mode: 'markers', marker: { size: 7, - line: { width: .5, color: 'rgba(255, 255, 255, 0.7)' }, + line: { width: .5, color: plotColors.text }, symbol: Symbolway[index % Symbolway.length], }, // Replicating the clean D3/Observable tooltip @@ -53,88 +61,77 @@ export default function PlumeEvents({ settings, widgetID}) { 'Date (UTC): %{x|%b %d %Y %H:%M}
' + 'Plume Height (m): %{y}' })); - }, [apiData]); + }, [apiData, plotColors]); const layout = useMemo(() => { - return { autosize: true, paper_bgcolor: 'transparent', // Let Chakra UI background show through plot_bgcolor: 'transparent', - font: { color: '#d1d5db' }, // gray.300 + font: { color: plotColors.muted }, 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: { title: { text: 'Date (UTC)', - font: { color: 'white', size: 12 } + font: { color: plotColors.text, size: 12 } }, type: 'date', - tickformat: '%b %d
%Y', // D3 requested %b %d \n %Y formatting - gridcolor: '#4b5563', - zerolinecolor: '#4b5563', + tickformat: '%b %d
%Y', + gridcolor: plotColors.grid, + zerolinecolor: plotColors.grid, automargin: true, }, yaxis: { title: { text: 'Plume Height', - font: { color: 'white', size: 12 } + font: { color: plotColors.text, size: 12 } }, type: 'linear', - gridcolor: '#4b5563', - zerolinecolor: '#ffffff', + gridcolor: plotColors.grid, + zerolinecolor: plotColors.text, automargin: true, }, - // legend: { - // orientation: 'h', - // y: -0.2, - // x: 0.5, - // xanchor: 'center' - // }, hovermode: 'closest', shapes: [ { type: 'line', x0: 0, x1: 1, - xref: 'paper', // Spans entire chart width + xref: 'paper', y0: 0, y1: 0, yref: 'y', - line: { color: 'white', width: 1 } + line: { color: plotColors.text, width: 1 } } ], }; - }, []); + }, [plotColors]); if (isLoading) return ( - + ); if (error) return ( - + {error} ); if (plotTraces.length === 0) return ( - - No data available for current filter. + + No data available for current filter. ); return ( - + diff --git a/client/src/components/Widgets/RemoteSensing.jsx b/client/src/components/Widgets/RemoteSensing.jsx index 3adbb27..38ed592 100644 --- a/client/src/components/Widgets/RemoteSensing.jsx +++ b/client/src/components/Widgets/RemoteSensing.jsx @@ -52,25 +52,25 @@ export default function RemoteSensing({ settings, widgetID }) { }, [settings, data, widgetID, updateSettings]); if (isLoading) return ( - + ); if (error) return ( - + {error} ); if (data.length === 0) return ( - - No Remote Sensing data available this week. + + No Remote Sensing data available this week. ); return ( - + {data.map((item, index) => { if (thisWidget?.size === "large" || thisWidget?.size === "medium") return ( @@ -80,14 +80,14 @@ export default function RemoteSensing({ settings, widgetID }) { <> - + {item?.volcano_name} - {item?.country} + {item?.country} - + {item?.report_time} @@ -95,10 +95,10 @@ export default function RemoteSensing({ settings, widgetID }) { {/* This box needs to be at h="100%" for the pdf toolbar to appear */} - - - {item?.summary} - Source: {item?.sender} + + + {item?.summary} + Source: {item?.sender} {item?.files?.map((file, index) => { @@ -107,15 +107,15 @@ export default function RemoteSensing({ settings, widgetID }) {
- {file?.name} - PDF + {file?.name} + PDF - + Your browser cannot display this PDF directly. @@ -132,8 +132,8 @@ export default function RemoteSensing({ settings, widgetID }) {
- {file?.name} - PNG + {file?.name} + PNG {file?.name} @@ -146,7 +146,7 @@ export default function RemoteSensing({ settings, widgetID }) { <>
- {file?.name} + {file?.name}
@@ -162,17 +162,17 @@ export default function RemoteSensing({ settings, widgetID }) { ) else if (thisWidget?.size === "small") return ( - - + + {item?.volcano_name} - {item?.country} + {item?.country} {item?.report_time} @@ -185,7 +185,6 @@ export default function RemoteSensing({ settings, widgetID }) { ); } - function ascTimeVolcanoSort(a, b) { if (a.volcano_name < b.volcano_name) return -1 else if (a.volcano_name > b.volcano_name) return 1 diff --git a/client/src/components/Widgets/SeismicEvents.jsx b/client/src/components/Widgets/SeismicEvents.jsx index d01dd75..3e3259d 100644 --- a/client/src/components/Widgets/SeismicEvents.jsx +++ b/client/src/components/Widgets/SeismicEvents.jsx @@ -1,10 +1,18 @@ import VdapApi from '../../hooks/useVdapApi.js'; import { useMemo } from 'react'; import Plot from 'react-plotly.js'; -import {Box, Flex, Spinner, Text} from "@chakra-ui/react"; +import { Box, Flex, Spinner, Text } from "@chakra-ui/react"; import useBuildUrl from "@/hooks/useBuildUrl.js"; +// 1. Import Chakra color mode and your central color dictionary +import { useColorMode } from "@/components/ui/color-mode.jsx"; +import { rawWidgetColors } from '../../theme.js'; + export default function SeismicEvents({ settings, widgetID}) { + const { colorMode } = useColorMode(); + // 2. Assign the active palette based on the current mode + const plotColors = colorMode === 'dark' ? rawWidgetColors.dark : rawWidgetColors.light; + // build url, defaults to 7 days past const url = useBuildUrl('/api/v2/seismic_events', settings?.startDate, settings?.endDate); @@ -44,7 +52,7 @@ export default function SeismicEvents({ settings, widgetID}) { mode: 'markers', marker: { size: 7, - line: { width: .5, color: 'rgba(255, 255, 255, 0.7)' }, + line: { width: .5, color: plotColors.text }, symbol: Symbolway[index % Symbolway.length], }, // Replicating the clean D3/Observable tooltip @@ -53,48 +61,36 @@ export default function SeismicEvents({ settings, widgetID}) { 'Date (UTC): %{x|%b %d %Y %H:%M}
' + 'Number of Seismic Events: %{y}' })); - }, [apiData]); + }, [apiData, plotColors]); const layout = useMemo(() => { - return { autosize: true, paper_bgcolor: 'transparent', // Let Chakra UI background show through plot_bgcolor: 'transparent', - font: { color: '#d1d5db' }, // gray.300 + font: { color: plotColors.muted }, margin: { t: 40, r: 20, l: 20, b: 20 }, - // title: { - // text: '', - // font: { size: 16, color: 'white' }, - // x: 0.05 - // }, xaxis: { title: { text: 'Date (UTC)', - font: { color: 'white', size: 12 } + font: { color: plotColors.text, size: 12 } }, type: 'date', tickformat: '%b %d
%Y', // D3 requested %b %d \n %Y formatting - gridcolor: '#4b5563', - zerolinecolor: '#4b5563', + gridcolor: plotColors.grid, + zerolinecolor: plotColors.grid, automargin: true, }, yaxis: { title: { text: 'Seismic Count', - font: { color: 'white', size: 12 } + font: { color: plotColors.text, size: 12 } }, type: 'linear', - gridcolor: '#4b5563', - zerolinecolor: '#ffffff', + gridcolor: plotColors.grid, + zerolinecolor: plotColors.text, automargin: true, }, - // legend: { - // orientation: 'h', - // y: -0.2, - // x: 0.5, - // xanchor: 'center' - // }, hovermode: 'closest', shapes: [ { @@ -105,33 +101,34 @@ export default function SeismicEvents({ settings, widgetID}) { y0: 0, y1: 0, yref: 'y', - line: { color: 'white', width: 1 } + line: { color: plotColors.text, width: 1 } } ], }; - }, []); + }, [plotColors]); if (isLoading) return ( - + ); if (error) return ( - + {error} ); if (plotTraces.length === 0) return ( - - No data available for current filter. + + No data available for current filter. ); return ( - + b.obs_fl - a.obs_fl); // Efficiently group into Plotly traces by volcano @@ -44,102 +48,90 @@ export default function VaaActivity7Day({ settings, widgetID }) { mode: 'markers', marker: { size: 7, - line: { width: .5, color: 'rgba(255, 255, 255, 0.7)' }, + line: { width: 0.5, color: plotColors.text }, symbol: Symbolway[index % Symbolway.length], }, // Replicating the clean Observable tooltip hovertemplate: - `${volcanoName}
` + - 'Date: %{x|%b %d %Y %H:%M}
' + - 'Flight Level: %{y}' + `${volcanoName}
` + + 'Date: %{x|%b %d %Y %H:%M}
' + + 'Flight Level: %{y}' })); - }, [apiData]); + }, [apiData, plotColors]); // 2. Build the Layout (Styling and Grid) const layout = useMemo(() => { - return { autosize: true, - paper_bgcolor: 'transparent', // Let Chakra UI background show through + paper_bgcolor: 'transparent', plot_bgcolor: 'transparent', - font: { color: '#d1d5db' }, // gray.300 + font: { color: plotColors.muted }, 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: { title: { text: 'Date (UTC)', - font: { color: 'white', size: 12 } + font: { color: plotColors.text, size: 12 } }, type: 'date', - tickformat: '%b %d
%Y %H:%M', // Multiline ticks from Observable - gridcolor: '#4b5563', - zerolinecolor: '#4b5563', + tickformat: '%b %d
%Y %H:%M', + gridcolor: plotColors.grid, + zerolinecolor: plotColors.grid, automargin: true, }, yaxis: { title: { text: 'Flight Level', - font: { color: 'white', size: 12 } + font: { color: plotColors.text, size: 12 } }, type: plotYAxisType, - gridcolor: '#4b5563', - zerolinecolor: '#ffffff', + gridcolor: plotColors.grid, + zerolinecolor: plotColors.text, automargin: true, }, - // legend: { - // orientation: 'h', - // y: -0.2, - // x: 0.5, - // xanchor: 'center' - // }, hovermode: 'closest', shapes: [ { type: 'line', x0: 0, x1: 1, - xref: 'paper', // Spans entire chart width + xref: 'paper', y0: 0, y1: 0, yref: 'y', - line: { color: 'white', width: 1 } + line: { color: plotColors.text, width: 1 } } ], }; - }, [apiData, plotYAxisType]); + }, [plotYAxisType, plotColors]); if (isLoading) return ( - - - + + + ); if (error) return ( - - {error} - + + {error} + ); if (plotTraces.length === 0) return ( - - No flight level data available. - + + No flight level data available. + ); return ( - - - + + + ); } \ No newline at end of file diff --git a/client/src/components/Widgets/settings/AlertChangeSettings.jsx b/client/src/components/Widgets/settings/AlertChangeSettings.jsx index bc1103b..b4b041f 100644 --- a/client/src/components/Widgets/settings/AlertChangeSettings.jsx +++ b/client/src/components/Widgets/settings/AlertChangeSettings.jsx @@ -4,21 +4,23 @@ import {useDashboardStore} from "@/store/useDashboardStore.jsx"; export default function IvansAlertsSettings({ widgetId, currentSettings }) { const updateSettings = useDashboardStore(state => state.updateWidgetSettings); + console.log("currentSettings:", currentSettings); + return ( - + {/* SLIDER DATE RANGE SELECTION */} { + console.log("change end", e); const day = new Date(Date.now() - (e.value[0])*86400000).toLocaleDateString('en-CA'); - console.log("new start: ", day); updateSettings(widgetId, {...currentSettings, startDate: day}); }} > - {currentSettings?.endDate} to {currentSettings?.startDate} + {currentSettings?.startDate} to {currentSettings?.endDate} diff --git a/client/src/components/dashboard/SettingsButton.jsx b/client/src/components/dashboard/SettingsButton.jsx index f49d6ae..0adec2e 100644 --- a/client/src/components/dashboard/SettingsButton.jsx +++ b/client/src/components/dashboard/SettingsButton.jsx @@ -38,9 +38,11 @@ export function SettingsButton() { const [canScrollRight, setCanScrollRight] = useState(false); // Pull the glass container styles for the dropdown and modal - const panelRecipe = useSlotRecipe({key: "glassPanel"}); - const panelStyles = panelRecipe({layout: "dropdown"}); - const modalStyles = panelRecipe({layout: "popover"}); + const popoverRecipe = useSlotRecipe({ key: "glassPopover" }); + const popoverStyles = popoverRecipe(); + + const dialogRecipe = useSlotRecipe({ key: "glassDialog" }); + const dialogStyles = dialogRecipe(); const [isLibraryOpen, setIsLibraryOpen] = useState(false); @@ -130,7 +132,7 @@ export function SettingsButton() { - + */} - - + + + Widgets Shop - + {/* HORIZONTAL SCROLLABLE MENU OF WIDGET CATEGORIES */} @@ -246,7 +249,6 @@ export function SettingsButton() { h="24px" flexShrink={0} borderRadius="full" - // bg={{base: "blue.50", _dark: "whiteAlpha.200"}} fontWeight="black" fontSize="xs" bg='transparent' diff --git a/client/src/pages/home/tabs/MyDashboard.jsx b/client/src/pages/home/tabs/MyDashboard.jsx index 8fcc8f8..3836ecf 100644 --- a/client/src/pages/home/tabs/MyDashboard.jsx +++ b/client/src/pages/home/tabs/MyDashboard.jsx @@ -29,11 +29,14 @@ export default function MyDashboard() { const hasHydrated = useRef(false); // 1. Pull the global slot recipes - const recipe = useSlotRecipe({key: "glassPanel"}); - const styles = recipe({ layout: "widget" }); - const popoverStyles = recipe({ layout: "popover" }); + const widgetRecipe = useSlotRecipe({ key: "widget" }); + const popoverRecipe = useSlotRecipe({ key: "glassPopover" }); - // 2. Populate widgets + // 2. Pass the editing state directly into the widget recipe! + const widgetStyles = widgetRecipe({ mode: isEditing ? "editing" : undefined }); + const popoverStyles = popoverRecipe(); + + // 3. Populate widgets useEffect(() => { if (user?.preferences?.dashboard_layout && !hasHydrated.current) { const savedLayout = user.preferences.dashboard_layout; @@ -55,7 +58,7 @@ export default function MyDashboard() { isResizable: false // The magic lock! No resizing allowed globally. })); - // 3. The component mapper + // 4. The component mapper const renderWidget = (widget) => { const registryEntry = WIDGET_REGISTRY[widget.type]; @@ -67,7 +70,7 @@ export default function MyDashboard() { return ( <> - + {registryEntry.name} @@ -83,7 +86,7 @@ export default function MyDashboard() { {/* NEW: The Positioner lifts the content out of the Flexbox flow */} - + }> - + @@ -105,7 +108,7 @@ export default function MyDashboard() { // Fallback if someone asks for a widget that doesn't exist return ( - + Error Widget '{widget.type}' not found. @@ -129,13 +132,7 @@ export default function MyDashboard() { {widgets.map(widget => ( {/* THE DELETE BUTTON */} @@ -163,7 +160,7 @@ export default function MyDashboard() { {/* SUSPENSE BOUNDARY wrapping the render function */} + }> diff --git a/client/src/theme.js b/client/src/theme.js index 53edb4f..517333b 100644 --- a/client/src/theme.js +++ b/client/src/theme.js @@ -1,11 +1,25 @@ import {defineSlotRecipe, createSystem, defaultConfig, defineConfig, defineRecipe} from "@chakra-ui/react"; -// CONSOLIDATED ✔ +// REUSABLE GLASS SURFACE +const glassSurface = { + bg: {base: "rgba(255, 255, 255, 0.4)", _dark: "rgba(255, 255, 255, 0.04)"}, + backdropFilter: "blur(12px)", + WebkitBackdropFilter: "blur(12px)", + border: "1px solid", + borderColor: {base: "gray.200", _dark: "whiteAlpha.200"}, + boxShadow: "sm", +}; + +/* ========================================================================== + RECIPES + ========================================================================== */ + +// 1. Header const headerRecipe = defineSlotRecipe({ slots: ["root", "search_bar", "user_info"], base: { root: { - bg: "{transparent}", + bg: "transparent", color: "text", width: "100%", height: "100%", @@ -20,7 +34,7 @@ const headerRecipe = defineSlotRecipe({ // input element in search bar component "& input": { width: "410px", - } + }, }, user_info: { flexDirection: "row", @@ -32,21 +46,29 @@ const headerRecipe = defineSlotRecipe({ // The Text component "& p": { fontWeight: "600", - fontSize: "md" // equivalent to 16px - } - } + fontSize: "md", + }, + }, }, }); -// CONSOLIDATED ✔ +// 2. Sidebar const sidebarRecipe = defineSlotRecipe({ - slots: ["root", "logo", "logo_text", "footer", "button_text", "buttons", "collapse_button", "color_mode_button"], + slots: [ + "root", + "logo", + "logo_text", + "footer", + "button_text", + "buttons", + "collapse_button", + "color_mode_button", + ], base: { root: { // 1. Semi-transparent backgrounds bg: { - // base: "linear-gradient(to bottom right, #99000040 20%, #FF660040 60%, #EEEE0040 100%)", // this is too pale for light mode imo - base: "linear-gradient(to bottom right, #97282880 20%, #EB7F3160 60%, #FCAD3840 100%)", // colors from: https://colorhunt.co/palette/972828e45742eb7f31fcad38 + base: "linear-gradient(to bottom right, #97282880 20%, #EB7F3160 60%, #FCAD3840 100%)", _dark: "linear-gradient(to bottom right, #9900003C 20%, #FF66003C 60%, #FFC0003C 100%)" }, @@ -62,8 +84,6 @@ const sidebarRecipe = defineSlotRecipe({ borderRight: "none", borderRadius: "2xl", - // boxShadow: "0 4px 30px rgba(0, 0, 0, 0.1)", - // boxShadow: "-10px 4px 30px rgba(0, 0, 0, 0.1)", width: "100%", height: "calc(100% - 16px)", margin: "8px", @@ -92,18 +112,18 @@ const sidebarRecipe = defineSlotRecipe({ "& svg": { transition: "stroke-width 0.4s ease", - flexShrink: "0" + flexShrink: "0", }, _hover: { bg: "rgba(255, 255, 255, 0.2)", color: "{text}", "& svg": { - strokeWidth: "3" + strokeWidth: "3", }, "& span": { fontWeight: "700", - } + }, }, fontSize: "l", @@ -122,12 +142,8 @@ const sidebarRecipe = defineSlotRecipe({ _hover: { bg: "{MainBackground}", color: "{textInverted}", - "& svg": { - strokeWidth: "2" // Keeps the active icon at normal thickness on hover - }, - "& span": { - fontWeight: "500", - } + "& svg": { strokeWidth: "2" }, + "& span": { fontWeight: "500" }, }, // The Top Inverted Curve @@ -156,7 +172,7 @@ const sidebarRecipe = defineSlotRecipe({ WebkitMaskImage: "radial-gradient(circle at bottom left, transparent 25px, black 25.5px)", maskImage: "radial-gradient(circle at bottom left, transparent 25px, black 25.5px)", pointerEvents: "none", - } + }, }, }, logo: { @@ -197,30 +213,8 @@ const sidebarRecipe = defineSlotRecipe({ _hover: { bg: "transparent", }, - } - } - // opened_theme_button: { - // width: "100%", - // align: "center", - // justifyContent: "right", - // mr: 6, - // // "& button": { - // // alignSelf: "center", - // // transition: "opacity 0.3s ease, visibility 0.3s ease, margin 0.3s ease", - // // _hover: { - // // bg: "transparent", - // // } - // // } - // }, - // closed_theme_button: { - // // ml: 0.9, - // // "& button": { - // // transition: "opacity 0.3s ease, visibility 0.3s ease, margin 0.3s ease", - // // _hover: { - // // bg: "transparent", - // // } - // // } - // }, + }, + }, }, variants: { state: { @@ -250,7 +244,7 @@ const sidebarRecipe = defineSlotRecipe({ } }); -// CONSOLIDATED ✔ +// 3. Dashboard Shell const dashboardRecipe = defineRecipe({ base: { height: "100vh", @@ -265,7 +259,7 @@ const dashboardRecipe = defineRecipe({ } }); -// CONSOLIDATED ✔ +// 4. Home Page Tabs const homeRecipe = defineSlotRecipe({ slots: ["root", "header", "body"], base: { @@ -290,305 +284,52 @@ const homeRecipe = defineSlotRecipe({ "& > div": { alignItems: "center", gap: 2, - - // icon in each tab button - "& svg": { - // TODO: bring over svg styling - } - } + }, }, body: { flex: 1, overflow: "auto", - } + }, }, -}) +}); -// const canvasRecipe = defineRecipe({ -// base: { -// bg: "{transparent}", -// zIndex: "-1", -// color: "{text}" -// } -// }); - -// const widgetRecipe = defineSlotRecipe({ -// slots: ["root", "header", "canvas"], -// base: { -// root: { -// flexDirection: "column", -// gap: "1", -// position: "relative", -// bg: "base100", -// borderRadius: "md", -// p: "1", -// paddingBottom: "2", -// border: "1px solid", -// borderColor: "{base300}", -// textAlign: "center", -// height: "100%", -// // position: "fixed" -// }, -// header: { -// flexShrink: "0", -// bg: "{base300}", -// color: "{text}", -// cursor: "move", -// borderRadius: "sm", -// whiteSpace: "nowrap", -// textAlign: "left", -// pl: "2" -// }, -// canvas: { -// flex: "1 1 auto", -// bg: "transparent", -// color: "{text}", -// cursor: "default", -// borderRadius: "md", -// overflow: "auto", -// } -// } -// }); - -// USED IN COMPONENTS: -// * Settings Button -// * My Dashboard -const glassPanelRecipe = defineSlotRecipe({ +// 5. Dashboard Widget Card +const widgetRecipe = defineSlotRecipe({ slots: ["root", "header", "body"], - - // 🧊 THE GLASS MATH (Applies to everything) base: { root: { display: "flex", flexDirection: "column", position: "relative", + ...glassSurface, + width: "100%", + height: "100%", + borderRadius: "3xl", overflow: "hidden", - bg: {base: "rgba(255, 255, 255, 0.4)", _dark: "rgba(255, 255, 255, 0.04)"}, - backdropFilter: "blur(12px)", - WebkitBackdropFilter: "blur(12px)", - border: "1px solid", - borderColor: {base: "gray.200", _dark: "whiteAlpha.200"}, - boxShadow: "sm", - } - }, - - // 📏 THE LAYOUTS (Applies selectively) - variants: { - layout: { - widget: { - root: { - width: "100%", - height: "100%", - borderRadius: "3xl", - }, - header: { - display: "flex", - justifyContent: "space-between", - alignItems: "center", - px: 3, - pt: 2, - pb: 0, - - "& > p": { - color: "titleColor", - fontWeight: "500", - fontSize: "16px", - - // the following three specifications are equivalent to Chakra's 'truncate' prop - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "clip", - }, - - "& > button": { - borderRadius: "xl", - - _hover: { - color: "white", - bg: "gray.500", - }, - } - }, - body: { - flex: "1", - overflow: "hidden", - position: "relative", - px: 1, - pb: 1, - pt: 0 - } - }, - dropdown: { - root: { - borderRadius: "xl", - p: 1 - } - // (Header and body slots are ignored here because dropdowns don't use them) - }, - popover: { - root: { - borderRadius: "2xl", - p: 0, - boxShadow: "xl", - minW: "240px", - maxW: "60%", - minH: "80%", - overflow: "hidden", - // OVERRIDES: Make the background 85-90% opaque instead of 40% - bg: { - base: "rgba(255, 255, 255, 0.75)", - _dark: "rgba(20, 22, 25, 0.8)" - }, - - // OVERRIDES: Increase the blur just to smooth out any stubborn gridlines - backdropFilter: "blur(15px)", - WebkitBackdropFilter: "blur(15px)", - - - // IMPORTANT: everything below is specific to the widget menu found in the settingsButton component - // dialog header - "& > div.chakra-dialog__header": { - bg: "transparent", - color: { - base: "gray.800", - _dark: "white" - }, - py: 4, - borderColor: { - base: "gray.200", - _dark: "whiteAlpha.200" - }, - - // title - "& h2": { - fontSize: "xl", - fontWeight: "bold" - }, - }, - - // dialog body - "& > div.chakra-dialog__body": { - p: 0, - - // box container of the widget category menu - "& div.chakra-tabs__root > div:first-child": { - position: "relative", - display: "flex", - maxW: "100%", - mx: 5, - mt: 4, - mb: 6 - }, - - // the horizontal widget categories menu - "& div.chakra-tabs__list": { - bg: { - base: "rgba(0, 0, 0, 0.04)", - _dark: "rgba(255, 255, 255, 0.05)" - }, - p: 1, - borderRadius: "full", - display: "flex", - flexWrap: "nowrap", - overflowX: "auto", - maxW: "100%", - border: "1px solid", - borderColor: { - base: "blackAlpha.50", - _dark: "whiteAlpha.100" - }, - scrollbarWidth: "none", - "&::-webkit-scrollbar": {display: "none"}, - - // the pill shaped menu options - "& button.chakra-tabs__trigger": { - px: 5, - py: 2, - borderRadius: "full", - whiteSpace: "nowrap", - flexShrink: 0, - color: { - base: "gray.500", - _dark: "gray.400" - }, - fontWeight: "medium", - transition: "background-color 0.2s, color 0.2s, box-shadow 0.2s", - _hover: { - color: { - base: "gray.800", - _dark: "gray.200" - }, - }, - _selected: { - bg: { - base: "white", - _dark: "rgba(255, 255, 255, 0.12)" - }, - color: { - base: "blue.600", - _dark: "blue.200" - }, - boxShadow: "sm", - fontWeight: "bold", - } - } - }, - - // base styling applied to both chevron navigators - "& div.chakra-tabs__root > div:first-child div:has(svg)": { - position: "absolute", - top: "0", - bottom: "0", - w: "50px", - alignItems: "center", - justifyContent: "center", - zIndex: 2, - cursor: "pointer", - bg: { - base: "rgba(255, 255, 255, 0.7)", - _dark: "rgba(30, 30, 30, 0.8)" - }, - color: { - base: "gray.600", - _dark: "gray.300" - }, - backdropFilter: "blur(1px)", - transition: "background-color 0.2s", - - _hover: { - color: { - base: "blue.500", - _dark: "blue.300" - }, - bg: { - base: "rgba(255, 255, 255, 0.9)", - _dark: "rgba(40, 40, 40, 0.95)" - }, - }, - - "&:has(.lucide-chevron-left)": { - left: 0, - borderTopLeftRadius: "full", - borderBottomLeftRadius: "full", - // Bites a transparent circle out of the flat right side! - WebkitMaskImage: "radial-gradient(circle at 100% 50%, transparent 30px, black 19px)", - maskImage: "radial-gradient(circle at 100% 50%, transparent 30px, black 35px)", - }, - - "&:has(.lucide-chevron-right)": { - right: 0, - borderTopRightRadius: "full", - borderBottomRightRadius: "full", - // THE MAGIC: Bites a transparent circle out of the flat left side! - WebkitMaskImage: "radial-gradient(circle at 0% 50%, transparent 30px, black 19px)", - maskImage: "radial-gradient(circle at 0% 50%, transparent 30px, black 35px)", - } - } - - } - } - } }, + header: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + px: 3, + pt: 2, + pb: 0, + "& > p": { + color: "titleColor", + fontWeight: "500", + fontSize: "16px", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "clip", + }, + "& > button": { + borderRadius: "xl", + _hover: {color: "white", bg: "gray.500"}, + }, + }, + body: {flex: "1", overflow: "hidden", position: "relative", px: 1, pb: 1, pt: 0}, + }, + variants: { mode: { editing: { root: { @@ -597,16 +338,116 @@ const glassPanelRecipe = defineSlotRecipe({ borderWidth: "2px", borderColor: "orange.300", cursor: "grab", - } - } - } + }, + }, + }, }, - - defaultVariants: { - layout: "widget" - } }); +// 6. TRUE POPOVER: Compact floating card for 3-dots menus +const glassPopoverRecipe = defineSlotRecipe({ + slots: ["content", "arrow", "positioner"], + base: { + content: { + ...glassSurface, + borderRadius: "xl", + p: 3, + boxShadow: "lg", + bg: {base: "rgba(255, 255, 255, 0.85)", _dark: "rgba(20, 22, 25, 0.85)"}, + backdropFilter: "blur(15px)", + WebkitBackdropFilter: "blur(15px)", + }, + }, +}); + +// 7. MODAL DIALOG: Large view for the Widget Shop +const glassDialogRecipe = defineSlotRecipe({ + slots: ["content", "header", "body", "backdrop"], + base: { + backdrop: { + bg: "rgba(0, 0, 0, 0.4)", + backdropFilter: "blur(4px)", + WebkitBackdropFilter: "blur(4px)", + }, + content: { + display: "flex", + flexDirection: "column", + position: "relative", + ...glassSurface, + borderRadius: "2xl", + p: 2, + boxShadow: "xl", + minW: "240px", + maxW: "60%", + minH: "80%", + overflow: "hidden", + bg: {base: "rgba(255, 255, 255, 0.75)", _dark: "rgba(20, 22, 25, 0.8)"}, + backdropFilter: "blur(15px)", + WebkitBackdropFilter: "blur(15px)", + }, + header: { + bg: "transparent", + color: {base: "gray.800", _dark: "white"}, + py: 4, + borderColor: {base: "gray.200", _dark: "whiteAlpha.200"}, + "& h2": {fontSize: "xl", fontWeight: "bold"}, + }, + body: { + p: 0, + "& div.chakra-tabs__root > div:first-child": { + position: "relative", + display: "flex", + maxW: "100%", + mx: 5, + mt: 4, + mb: 6 + }, + "& div.chakra-tabs__list": { + bg: {base: "rgba(0, 0, 0, 0.04)", _dark: "rgba(255, 255, 255, 0.05)"}, + p: 1, borderRadius: "full", display: "flex", flexWrap: "nowrap", overflowX: "auto", maxW: "100%", + border: "1px solid", borderColor: {base: "blackAlpha.50", _dark: "whiteAlpha.100"}, + scrollbarWidth: "none", "&::-webkit-scrollbar": {display: "none"}, + "& button.chakra-tabs__trigger": { + px: 5, py: 2, borderRadius: "full", whiteSpace: "nowrap", flexShrink: 0, + color: {base: "gray.500", _dark: "gray.400"}, + fontWeight: "medium", transition: "background-color 0.2s, color 0.2s, box-shadow 0.2s", + _hover: {color: {base: "gray.800", _dark: "gray.200"}}, + _selected: { + bg: {base: "white", _dark: "rgba(255, 255, 255, 0.12)"}, + color: {base: "blue.600", _dark: "blue.200"}, + boxShadow: "sm", + fontWeight: "bold" + } + } + }, + "& div.chakra-tabs__root > div:first-child div:has(svg)": { + position: "absolute", top: "0", bottom: "0", w: "50px", alignItems: "center", justifyContent: "center", + zIndex: 2, cursor: "pointer", bg: {base: "rgba(255, 255, 255, 0.7)", _dark: "rgba(30, 30, 30, 0.8)"}, + color: {base: "gray.600", _dark: "gray.300"}, backdropFilter: "blur(1px)", transition: "background-color 0.2s", + _hover: { + color: {base: "blue.500", _dark: "blue.300"}, + bg: {base: "rgba(255, 255, 255, 0.9)", _dark: "rgba(40, 40, 40, 0.95)"} + }, + "&:has(.lucide-chevron-left)": { + left: 0, + borderTopLeftRadius: "full", + borderBottomLeftRadius: "full", + WebkitMaskImage: "radial-gradient(circle at 100% 50%, transparent 30px, black 19px)", + maskImage: "radial-gradient(circle at 100% 50%, transparent 30px, black 35px)" + }, + "&:has(.lucide-chevron-right)": { + right: 0, + borderTopRightRadius: "full", + borderBottomRightRadius: "full", + WebkitMaskImage: "radial-gradient(circle at 0% 50%, transparent 30px, black 19px)", + maskImage: "radial-gradient(circle at 0% 50%, transparent 30px, black 35px)" + }, + }, + }, + }, +}); + +// 6. Pill/Glass Expanding Tab Button const glassTabRecipe = defineRecipe({ base: { display: "flex", @@ -663,11 +504,30 @@ const glassTabRecipe = defineRecipe({ bg: "{gooeyButtonBackground}", cursor: "default", textDecoration: "none", - } - } - } + }, + }, + }, }); +/* ========================================================================== + RAW WIDGET COLORS (For Canvas/Plotly libraries that reject CSS variables) + ========================================================================== */ +export const rawWidgetColors = { + light: { + text: "#000000", + muted: "#4A5568", // gray.600 + grid: "rgba(0, 0, 0, 0.1)", + }, + dark: { + text: "#FFFFFF", + muted: "#A0AEC0", // gray.400 + grid: "rgba(255, 255, 255, 0.15)", + } +}; + +/* ========================================================================== + CONFIG & SYSTEM + ========================================================================== */ const config = defineConfig({ globalCss: { ".react-grid-placeholder": { @@ -706,36 +566,14 @@ const config = defineConfig({ "::-webkit-scrollbar-track": { background: "transparent", }, - // '.leaflet-tile': { - // _dark: { - // filter: "invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%)", - // } - // }, - '.leaflet-control-zoom-in, .leaflet-control-zoom-out, .leaflet-control-attribution, .leaflet-container': { + ".leaflet-control-zoom-in, .leaflet-control-zoom-out, .leaflet-control-attribution, .leaflet-container": { _dark: { filter: "invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%)", - } + }, }, - '.leaflet-control-attribution': { - _dark: { - filter: "invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%)", - } - }, - '.leaflet-container': { + ".leaflet-container": { fontFamily: "system-ui, sans-serif !important", - _dark: { - filter: "invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%)", - } }, - // ".leaflet-popup-content": { - // fontFamily: "georgia, system-ui, sans-serif !important" - // }, - // ".leaflet-control a": { - // fontFamily: "georgia, system-ui, sans-serif !important" - // }, - // ".leaflet-tooltip": { - // fontFamily: "georgia, system-ui, sans-serif !important" - // }, ".react-grid-item": { transition: "none !important" }, @@ -743,22 +581,25 @@ const config = defineConfig({ theme: { semanticTokens: { colors: { - // base100: {value: {base: "#f6f9faff", _dark: "#1617199e"}}, - // base200: {value: {base: "#e6ecf1ff", _dark: "#242424"}}, - // base300: {value: {base: "#d3dae4ff", _dark: "#0a0b0bff"}}, - // primary: {value: {base: "#2B98F8", _dark: "#2B98F8"}}, - // primaryLt: {value: {base: "#98cdffff", _dark: "#98cdffff"}}, - // secondaryBG: {value: {base: "#EDF1F1", _dark: "#121212"}}, - // border: {value: {base: "#000000", _dark: "#cbcbcb84"}}, - text: {value: {base: "#000000", _dark: "#FFFFFF"}}, - textInverted: {value: {base: "#000000", _dark: "#FFFFFF"}}, - // icon: {value: {base: "#000000", _dark: "#FFFFFF"}}, - + text: { value: { base: "#000000", _dark: "#FFFFFF" } }, + textInverted: { value: { base: "#000000", _dark: "#FFFFFF" } }, // Fixed: Properly inverted + MainBackground: { value: { base: "#F9F9F9", _dark: "#141414" } }, + titleColor: { value: { base: "gray.800", _dark: "#FFFFFF" } }, + iconColor: { value: { base: "gray.600", _dark: "gray.300" } }, // Fixed: Visible in dark mode + glassSeperator: { value: { base: "#0000001A", _dark: "#FFFFFF3F" } }, gooeyButtonBackground: {value: {base: "rgba(255, 255, 255, 0.1)", _dark: "rgba(255, 255, 255, 0.1)"}}, - MainBackground: {value: {base: "#F9F9F9", _dark: "#141414"}}, - titleColor: {value: {base: "gray.800", _dark: "#FFFFFF"}}, - iconColor: {value: {base: "gray.600", _dark: "#000000"}}, - glassSeperator: {value: {base: "#0000001A", _dark: "#FFFFFF3F"}}, + + // 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: { @@ -768,19 +609,17 @@ const config = defineConfig({ }, recipes: { dashboard: dashboardRecipe, - // canvas: canvasRecipe, - glassTab: glassTabRecipe + glassTab: glassTabRecipe, }, slotRecipes: { header: headerRecipe, sidebar: sidebarRecipe, - // widget: widgetRecipe, - glassPanel: glassPanelRecipe, home: homeRecipe, + widget: widgetRecipe, + glassPopover: glassPopoverRecipe, + glassDialog: glassDialogRecipe, }, }, }); - -// Define semantic tokens and export recipes export const system = createSystem(defaultConfig, config); \ No newline at end of file