diff --git a/client/src/App.jsx b/client/src/App.jsx
index 9ab481b..e507f47 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -33,12 +33,12 @@ function AppFoundation() {
height="100vh"
width="100vw"
>
-
-
-
-
+
+
+
+
@@ -48,8 +48,8 @@ function AppFoundation() {
}
const Placeholder = ({ name }) => (
-
- {name} Dashboard - Coming Soon
+
+ {name} Dashboard - Coming Soon
);
@@ -75,7 +75,7 @@ function App() {
}>
} />
- } />
+ } />
} />
} />
} />
diff --git a/client/src/components/Widgets/LlmAlChangesQuadPlot.jsx b/client/src/components/Widgets/LlmAlChangesQuadPlot.jsx
new file mode 100644
index 0000000..f8ff839
--- /dev/null
+++ b/client/src/components/Widgets/LlmAlChangesQuadPlot.jsx
@@ -0,0 +1,84 @@
+import VdapApi from '../../hooks/useVdapApi.js';
+import {Flex, Spinner, Text, Box, Center} from "@chakra-ui/react"; // Adjust path as needed
+
+
+export default function LlmAlChangesQuadPlot(props) {
+ console.log("3. Quad Plot loaded");
+ let tp_percent = 0;
+ let tn_percent = 0;
+ let fp_percent = 0;
+ let fn_percent = 0;
+
+ // Clean, single-line data fetching
+ const { data: apiData, isLoading, error } = VdapApi('/api/v2/llm_quadrant_plot');
+
+ if (isLoading) return (
+
+
+
+ );
+
+ if (error) return (
+
+ {error}
+
+ );
+
+ if (apiData.length === 0) return (
+
+ No alert level quad plot data available.
+
+ );
+
+ if (apiData) {
+ console.log(apiData);
+ tp_percent = ((apiData[0]['tp_count']/apiData[0]['tot_count'])*100).toFixed(2);
+ tn_percent = ((apiData[0]['tn_count']/apiData[0]['tot_count'])*100).toFixed(2);
+ fp_percent = ((apiData[0]['fp_count']/apiData[0]['tot_count'])*100).toFixed(2);
+ fn_percent = ((apiData[0]['fn_count']/apiData[0]['tot_count'])*100).toFixed(2);
+ }
+
+ return (
+
+
+ {/*Alert Level Changes Quad Plot*/}
+ All time
+
+
+
+
+
+ True Positive
+ {tp_percent}%
+ {apiData[0]['tp_count']}/{apiData[0]['tot_count']}
+
+
+
+
+ True Negative
+ {tn_percent}%
+ {apiData[0]['tn_count']}/{apiData[0]['tot_count']}
+
+
+
+
+
+
+ False Positive
+ {fp_percent}%
+ {apiData[0]['fp_count']}/{apiData[0]['tot_count']}
+
+
+
+
+ False Negative
+ {fn_percent}%
+ {apiData[0]['fn_count']}/{apiData[0]['tot_count']}
+
+
+
+
+
+ )
+
+}
\ No newline at end of file
diff --git a/client/src/components/Widgets/LlmAlChangesQuadPlot.stories.jsx b/client/src/components/Widgets/LlmAlChangesQuadPlot.stories.jsx
new file mode 100644
index 0000000..42ba8fc
--- /dev/null
+++ b/client/src/components/Widgets/LlmAlChangesQuadPlot.stories.jsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import { Box } from '@chakra-ui/react';
+import { http, HttpResponse } from 'msw'; // Import MSW
+import LlmAlChangesQuadPlot from './LlmAlChangesQuadPlot';
+
+export default {
+ title: 'Dashboard Widgets/LLM AL Changes Quad Plot',
+ component: LlmAlChangesQuadPlot,
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+const mockData = [
+ { tp: 50 },
+ { tn: 250 },
+ { fp: 10 },
+ { fn: 5 },
+ { tot_count: 315}
+];
+
+// Scenario LIVE: call our API for real
+export const LivePopulated = {};
+
+// Scenario 1: Successful API Call
+export const Populated = {
+ parameters: {
+ msw: {
+ handlers: [
+ // Intercept the exact URL your component is trying to fetch
+ http.get('*/api/v2/llm_quadrant_plot', () => {
+ // Return a 200 OK with the mock JSON
+ return HttpResponse.json(mockData);
+ }),
+ ],
+ },
+ },
+};
+
+// Scenario 2: The 500 Server Crash
+export const ServerCrash = {
+ parameters: {
+ msw: {
+ handlers: [
+ http.get('*/api/v2/llm_quad_plot', () => {
+ // Return a 500 Error
+ return new HttpResponse(null, { status: 500 });
+ }),
+ ],
+ },
+ },
+};
+
+// Scenario 3: Empty State (API works, but no data returned)
+export const EmptyData = {
+ parameters: {
+ msw: {
+ handlers: [
+ http.get('*/api/v2/llm_quad_plot', () => {
+ return HttpResponse.json([]);
+ }),
+ ],
+ },
+ },
+};
\ No newline at end of file
diff --git a/client/src/components/dashboard/SettingsButton.jsx b/client/src/components/dashboard/SettingsButton.jsx
index fa3aff3..751397f 100644
--- a/client/src/components/dashboard/SettingsButton.jsx
+++ b/client/src/components/dashboard/SettingsButton.jsx
@@ -1,153 +1,375 @@
-import { useState, useMemo } from "react";
+import {useState, useMemo, useRef, useEffect, useCallback} from "react";
import {
- Button,
- IconButton,
- Box,
- Text,
- Flex,
- Grid,
- Dialog,
- Tabs,
- Badge,
- Menu,
- HStack
+ Button,
+ Box,
+ Text,
+ Flex,
+ Grid,
+ Dialog,
+ Tabs,
+ Badge,
+ Menu,
+ HStack,
+ useRecipe,
+ useSlotRecipe
} from "@chakra-ui/react";
-import { Settings, Plus, LayoutGrid, Move, Save, X } from 'lucide-react';
-import { useDashboardStore } from "../../store/useDashboardStore.jsx";
-import { WIDGET_REGISTRY } from "../../constants/widgetRegistry.jsx";
+import {Settings, LayoutGrid, Move, Save, X, ChevronRight, ChevronLeft} from 'lucide-react';
+import {useDashboardStore} from "../../store/useDashboardStore.jsx";
+import {WIDGET_REGISTRY} from "../../constants/widgetRegistry.jsx";
+
+// π¨ NEW: Tiny helper to convert size keys into abbreviations
+const getSizeAbbreviation = (key) => {
+ const mapping = {small: "S", medium: "M", large: "L", xlarge: "XL"};
+ return mapping[key.toLowerCase()] || key.charAt(0).toUpperCase();
+};
export function SettingsButton() {
- // 1. βStore connections
- const isEditing = useDashboardStore(state => state.isEditing);
- const enterEditMode = useDashboardStore(state => state.enterEditMode);
+ // 1. βStore connections
+ const isEditing = useDashboardStore(state => state.isEditing);
+ const enterEditMode = useDashboardStore(state => state.enterEditMode);
+ const saveEdit = useDashboardStore(state => state.saveEdit);
+ const cancelEdit = useDashboardStore(state => state.cancelEdit);
+ const addWidget = useDashboardStore(state => state.addWidget);
- // This now handles both local UI lock AND database saving!
- const saveEdit = useDashboardStore(state => state.saveEdit);
+ const recipe = useRecipe({key: "glassTab"});
+ const tabStyles = recipe();
- const cancelEdit = useDashboardStore(state => state.cancelEdit);
- const addWidget = useDashboardStore(state => state.addWidget);
+ // 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 [isLibraryOpen, setIsLibraryOpen] = useState(false);
+ const [isLibraryOpen, setIsLibraryOpen] = useState(false);
- // Smart Add Handler
- const handleAddWidget = (widgetId, sizeKey) => {
- enterEditMode();
- addWidget(widgetId, sizeKey);
- };
+ // β¬
οΈ β‘οΈ This is the callbacks and scroll control to add the chevron arrows in the tab list
+ const tabsRef = useRef(null);
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
+ const [canScrollRight, setCanScrollRight] = useState(false);
- const categorizedWidgets = useMemo(() => {
- return Object.entries(WIDGET_REGISTRY).reduce((acc, [id, widget]) => {
- const cat = widget.category || 'General';
- if (!acc[cat]) acc[cat] = [];
- acc[cat].push({ id, ...widget });
- return acc;
- }, {});
- }, []);
+ const checkScroll = useCallback(() => {
+ if (tabsRef.current) {
+ const {scrollLeft, scrollWidth, clientWidth} = tabsRef.current;
+ setCanScrollLeft(scrollLeft > 0);
+ // We use -1 here to account for sub-pixel rendering in some browsers
+ setCanScrollRight(Math.ceil(scrollLeft + clientWidth) < scrollWidth - 1);
+ }
+ }, []);
- const categories = Object.keys(categorizedWidgets);
+ useEffect(() => {
+ if (isLibraryOpen) {
+ // Small timeout ensures the DOM has rendered the tabs before measuring
+ setTimeout(checkScroll, 50);
+ window.addEventListener('resize', checkScroll);
+ return () => window.removeEventListener('resize', checkScroll);
+ }
+ }, [isLibraryOpen, checkScroll]);
- return (
- <>
- {/* THE STATIC DROPDOWN MENU */}
-
-
- {
+ enterEditMode();
+ addWidget(widgetId, sizeKey);
+ };
+
+ const categorizedWidgets = useMemo(() => {
+ return Object.entries(WIDGET_REGISTRY).reduce((acc, [id, widget]) => {
+ const cat = widget.category || 'General';
+ if (!acc[cat]) acc[cat] = [];
+ acc[cat].push({id, ...widget});
+ return acc;
+ }, {});
+ }, []);
+
+ const categories = Object.keys(categorizedWidgets);
+
+ // use ref to capture html node for placing menu accordingly
+ const menuRef = useRef(null);
+ const getAnchorRect = () => {
+ if (!menuRef.current) return null;
+ return menuRef.current.getBoundingClientRect();
+ };
+
+ return (
+ <>
+ {isEditing ? (
+ // π EDIT MODE: The 3-Pill Layout
+
+ {/* 1. The Add Widget Button */}
+ setIsLibraryOpen(true)}
+ color={{base: "blue.500", _dark: "blue.300"}}
+ _hover={{bg: "rgba(66, 153, 225, 0.1)"}}
+ >
+
+ Widget Library
+
+
+ {/* 2. The Save Button */}
+
+
+ Save Layout
+
+
+ {/* 3. The Discard Button */}
+
+
+ Discard
+
+
+ ) : (
+ // π VIEW MODE: The Static Dropdown Menu
+
+
+
+ {/* size increased to 24 to match tabs. flexShrink: 0 is CRITICAL */}
+
+ Dashboard Settings
+
+
+
+
+
+
+ Edit Layout
+
+
+
+
+ setIsLibraryOpen(true)}
+ cursor="pointer"
+ _hover={{
+ bg: {base: "rgba(0, 0, 0, 0.05)", _dark: "rgba(255, 255, 255, 0.1)"},
+ WebkitTextStroke: "0.5px currentColor"
+ }}
+ >
+ Widget Library
+
+
+
+
+ )}
+
+ {/* πππ THE WIDGET LIBRARY MODAL (Controlled by isLibraryOpen) */}
+ setIsLibraryOpen(e.open)}
+ size="xl"
+ placement="center"
+ closeOnInteractOutside={true}
+ closeOnEscapeKeyDown={true}
+ >
+ {/* Soft blur can be added to the background behind the modal */}
+ {/**/}
+
+
+
+
+
+ Widgets Shop
+
+
+
+
+
+
+ {/* LEFT GLASS CRESCENT */}
+ {canScrollLeft && (
+ tabsRef.current.scrollBy({left: -200, behavior: 'smooth'})}
+ bg={{base: "rgba(255, 255, 255, 0.7)", _dark: "rgba(30, 30, 30, 0.8)"}}
+ backdropFilter="blur(1px)"
+ borderTopLeftRadius="full"
+ borderBottomLeftRadius="full"
+
+ // THE MAGIC: 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)"
+
+ color={{base: "gray.600", _dark: "gray.300"}}
+ 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)"}
+ }}
>
-
-
-
+ {/* Shifted slightly left to optically center it in the crescent mass */}
+
+
+ )}
+ {
+ if (e.currentTarget) {
+ e.currentTarget.scrollLeft += e.deltaY;
+ }
+ }}
+ >
+ {categories.map(cat => (
+
+ {cat}
+
+ ))}
+
+ {canScrollRight && (
+ tabsRef.current.scrollBy({left: 200, behavior: 'smooth'})}
+ bg={{base: "rgba(255, 255, 255, 0.7)", _dark: "rgba(30, 30, 30, 0.8)"}}
+ backdropFilter="blur(1px)"
+ borderTopRightRadius="full"
+ borderBottomRightRadius="full"
-
- {isEditing ? (
- <>
- {/* WIRE DIRECTLY TO saveEdit */}
-
- Save Layout
-
+ // 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)"
-
- Discard Changes
-
- >
- ) : (
-
- Edit Layout
-
- )}
+ color={{base: "gray.600", _dark: "gray.300"}}
+ 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)"}
+ }}
+ >
+ {/* Shifted slightly right to optically center it in the crescent mass */}
+
+
+ )}
-
+
+ {categories.map(cat => (
+
+
+ {categorizedWidgets[cat].map(widget => (
+
+ {widget.name}
+ {widget.description}
- setIsLibraryOpen(true)} cursor="pointer" _hover={{ bg: "gray.100" }}>
- Widget Library
-
-
-
+
+
+ Add to Dashboard:
+
+
+ {Object.keys(widget.allowedSizes).map(sizeKey => (
+ handleAddWidget(widget.id, sizeKey)}
+ color={{base: "blue.600", _dark: "blue.300"}}
+ >
+ {/* THE ABBREVIATION ICON: Always visible */}
+
+ {getSizeAbbreviation(sizeKey)}
+
- {/* THE WIDGET LIBRARY MODAL (Controlled by isLibraryOpen) */}
- setIsLibraryOpen(e.open)} size="xl" placement="center">
-
-
-
-
- Add Dashboard Widgets
-
-
-
-
-
- {categories.map(cat => (
- {cat}
- ))}
-
-
- {categories.map(cat => (
-
-
- {categorizedWidgets[cat].map(widget => (
-
- {widget.name}
- {widget.description}
-
-
- Add to Grid:
-
- {Object.keys(widget.allowedSizes).map(sizeKey => (
- handleAddWidget(widget.id, sizeKey)}
- colorScheme="gray"
- variant="subtle"
- px={3}
- py={1}
- borderRadius="full"
- cursor="pointer"
- _hover={{ bg: "blue.50", color: "blue.600", transform: "scale(1.05)" }}
- transition="all 0.2s"
- >
-
- {sizeKey.charAt(0).toUpperCase() + sizeKey.slice(1)}
-
- ))}
-
-
-
- ))}
-
-
- ))}
-
-
-
-
-
-
- >
- );
+ {/* THE EXPANDED TEXT: Only visible on hover */}
+ Add Widget
+
+ ))}
+
+
+
+ ))}
+
+
+ ))}
+
+
+
+
+
+
+ >
+ );
}
\ No newline at end of file
diff --git a/client/src/components/deprecated/AlertLevelWidget.jsx b/client/src/components/deprecated/AlertLevelWidget.jsx
deleted file mode 100644
index b3b3afb..0000000
--- a/client/src/components/deprecated/AlertLevelWidget.jsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import Widget from "./Widget.jsx";
-import { Fieldset, NativeSelect, For, Box, Field, Input, Button, Text, Flex } from "@chakra-ui/react";
-import { useForm } from "react-hook-form";
-import { useState } from "react";
-import { Asterisk } from 'lucide-react';
-
-export default function AlertLevel({id, isEditable, isDelete, DeleteWidget}) {
- const { register, handleSubmit, reset, formState: {errors} } = useForm();
- const [isSubmit, setIsSubmit] = useState(false);
-
- const onSubmit = (data) => {
- setIsSubmit(true);
- reset();
- }
-
- // Prop Forwarding
- const widgetProps = { id, isEditable, isDelete, DeleteWidget, isSubmit, setIsSubmit }
-
- return (
-
- {!isSubmit ? (
- // DISPLAYING INPUT FIElDS AND LABELS
-
-
-
-
- Volcano
-
-
-
-
- Country
-
-
-
- {(item) => (
-
- )}
-
-
-
-
-
-
-
-
-
-
- ):(
- // DISPLAYING ALERT LEVEL DATA
-
-
- Alert Level Data Coming Soon.
-
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/deprecated/CancelButton.jsx b/client/src/components/deprecated/CancelButton.jsx
deleted file mode 100644
index 28b68a7..0000000
--- a/client/src/components/deprecated/CancelButton.jsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Button } from "@chakra-ui/react";
-
-export function CancelButton ({onCancel}) {
- return (
-
- )
-}
\ No newline at end of file
diff --git a/client/src/components/deprecated/Canvas.jsx b/client/src/components/deprecated/Canvas.jsx
deleted file mode 100644
index 5b449f1..0000000
--- a/client/src/components/deprecated/Canvas.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-// DEPRECATED
-
-// import GlobalMap from "./views/GlobalMap";
-// import RegionalMap from "./views/RegionalMap";
-// import Volcano from "./views/Volcano";
-// import Admin from "./views/Admin";
-// import Account from "./views/Account"
-// import Home from "./views/Home";
-// import { DASHBOARD_VIEWS, ADMIN_VIEWS, VOLCANO_VIEWS } from "@/constants/viewKeys";
-// import { chakra } from "@chakra-ui/react";
-// import Personal from "./views/Personal";
-// import Security from "./views/Security";
-//
-// export default function Canvas(props) {
-// const { view } = props;
-//
-// return (
-//
-// {/* Check if view is a tab inside the Home Header */}
-// { DASHBOARD_VIEWS.includes(view) && (
-//
-// )}
-// {/* Check if view is a tab inside the Volcano Header */}
-// { VOLCANO_VIEWS.includes(view) && (
-//
-// )}
-// {/* Check if view is a tab inside the Admin Header */}
-// { ADMIN_VIEWS.includes(view) && (
-//
-// )}
-// {view === "global" && }
-// {view === "regional" && }
-// {view === "account" && }
-// {view === "settings" && }
-// {view === "personal" && }
-// {view === "security" && }
-//
-// );
-// }
\ No newline at end of file
diff --git a/client/src/components/deprecated/CustomResizeHandle.jsx b/client/src/components/deprecated/CustomResizeHandle.jsx
deleted file mode 100644
index 9fa3730..0000000
--- a/client/src/components/deprecated/CustomResizeHandle.jsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { MoveDiagonal2 } from 'lucide-react';
-import React from "react";
-
-const CustomResizeHandle = React.forwardRef((props, ref) => {
- const { handleAxis, isVisable, ...restProps } = props;
- if (!isVisable) return null;
- return (
-
-
-
- );
-});
-
-export default CustomResizeHandle;
\ No newline at end of file
diff --git a/client/src/components/deprecated/Dashboard.jsx b/client/src/components/deprecated/Dashboard.jsx
deleted file mode 100644
index 275e680..0000000
--- a/client/src/components/deprecated/Dashboard.jsx
+++ /dev/null
@@ -1,95 +0,0 @@
-// DEPRECATED
-
-// import Header from './Header/Header.jsx';
-// import Sidebar from './Sidebar/Sidebar.jsx';
-// import Canvas from './Canvas/Canvas.jsx';
-// import { Grid, GridItem, useRecipe, useDialog } from '@chakra-ui/react';
-// import { useState } from 'react';
-// import DialogPopup from './Canvas/CanvasComponents/DialogPopup.jsx';
-//
-// export default function Dashboard() {
-// const recipe = useRecipe({ key: "dashboard" });
-// const styles = recipe();
-//
-// const [darkMode, setDarkMode] = useState(false);
-// const [isEditable, setIsEditable] = useState(false); // editable=true means static=false (vice versa)
-// const [isDelete, setIsDelete] = useState(false);
-// const [originalLayout, setOriginalLayout] = useState([]);
-// const [originalWidgets, setOriginalWidgets] = useState([]);
-// const [view, setView] = useState("mydashboard");
-// const [layout, setLayout] = useState([]);
-// const [widgetArray, setWidgetArray] = useState([]);
-// const dialog = useDialog();
-//
-// const beginEditIfNeeded = () => {
-// if ( !isEditable ) {
-// setOriginalLayout(layout);
-// setOriginalWidgets(widgetArray);
-// }
-// };
-//
-// const onCancel = () => {
-// console.log("canceling changes...");
-// console.log(originalWidgets);
-// if (originalLayout.length) {
-// setLayout(originalLayout.map(item => ({
-// ...item,
-// static: true
-// })));
-// };
-// setWidgetArray(originalWidgets);
-// setIsEditable(false);
-// setIsDelete(false);
-// setOriginalLayout([]);
-// setOriginalWidgets([]);
-// }
-//
-// // Prevent navigation while in Edit mode without saving or cancelling
-// const handleViewChange = (nextView) => {
-// console.log("1. handling view change")
-// if (view === "mydashboard" && isEditable) {
-// console.log("2. currently editing");
-// dialog.setOpen(true);
-// return;
-//
-// // const confirmed = window.confirm(
-// // "You have unsaved changes. Discard them and continue?"
-// // );
-// // if (!confirmed) return;
-// // onCancel();
-// }
-// setView(nextView);
-// };
-//
-// const onLayoutChange = (newLayout) => setLayout(newLayout);
-//
-// // Prop Forwarding
-// const canvasProps = { view, layout, setLayout, setOriginalLayout, onLayoutChange, onChangeView: handleViewChange,
-// onCancel, isEditable, setIsEditable, isDelete, setIsDelete, widgetArray, setWidgetArray, originalWidgets, setOriginalWidgets, beginEditIfNeeded
-// };
-// const sidebarProps = { view, onChangeView: handleViewChange, darkMode, setDarkMode };
-// const headerProps = { onChangeView: handleViewChange };
-//
-// return (
-// <>
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-// >
-// );
-// }
\ No newline at end of file
diff --git a/client/src/components/deprecated/DeleteAllButton.jsx b/client/src/components/deprecated/DeleteAllButton.jsx
deleted file mode 100644
index 016d49a..0000000
--- a/client/src/components/deprecated/DeleteAllButton.jsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Button } from "@chakra-ui/react";
-
-export default function DeleteAllButton ({onDeleteAll}) {
- return (
-
- )
-}
\ No newline at end of file
diff --git a/client/src/components/deprecated/FirstWidgetButton.jsx b/client/src/components/deprecated/FirstWidgetButton.jsx
deleted file mode 100644
index 5c19f03..0000000
--- a/client/src/components/deprecated/FirstWidgetButton.jsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Box, Menu, Portal } from '@chakra-ui/react'
-import AlertLevel from "@/components/deprecated/AlertLevelWidget.jsx";
-
-export default function FirstWidgetButton({ appendWidget }) {
- const triggerItem = (
- <>
-
-
- Add Your First Widget
-
-
- >
- );
- const positioning = { placement: "bottom-middle" };
- // const WidgetMenuProps = { appendWidget, triggerItem, positioning };
- return (
- //
-
- {triggerItem}
-
-
-
- appendWidget(AlertLevel, 4, 7, 4, 7, 8, 14)} // width and height are passed through here for each widget
- >
- Alert Level
-
- CO2
- IVANS
- Plume Heights
- Alert Level Changes
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/deprecated/SaveButton.jsx b/client/src/components/deprecated/SaveButton.jsx
deleted file mode 100644
index 19365f0..0000000
--- a/client/src/components/deprecated/SaveButton.jsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Button } from "@chakra-ui/react";
-
-export function SaveButton ({onSave}) {
- return (
-
- )
-}
\ No newline at end of file
diff --git a/client/src/components/deprecated/Widget.jsx b/client/src/components/deprecated/Widget.jsx
deleted file mode 100644
index c05678f..0000000
--- a/client/src/components/deprecated/Widget.jsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { useSlotRecipe, chakra, Flex, CloseButton, Text, IconButton } from "@chakra-ui/react"
-import { Undo } from 'lucide-react';
-
-export default function Widget(props) {
- // deconstructing props
- const { id, title, isEditable, isDelete, DeleteWidget, isSubmit, setIsSubmit, children } = props;
-
- const recipe = useSlotRecipe({ key: "widget" });
- const styles = recipe();
-
- return (
-
- {/* HEADER */}
-
- {title}
- {isDelete ? (
- DeleteWidget(id)} size="xs" _hover={{bg: "tomato", color: "white"}}/>
- ):(
- isSubmit ? (
- setIsSubmit(false)} size="xs" bg="base300" cursor="pointer">
- ):null
- )}
-
-
- {/* CHILDREN */}
-
- {children}
-
-
- );
-}
-
diff --git a/client/src/components/deprecated/WidgetMenu.jsx b/client/src/components/deprecated/WidgetMenu.jsx
deleted file mode 100644
index 140af76..0000000
--- a/client/src/components/deprecated/WidgetMenu.jsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Menu, Portal } from "@chakra-ui/react";
-import AlertLevel from "./AlertLevelWidget.jsx";
-
-export default function WidgetMenu({appendWidget, triggerItem, positioning}) {
- return (
-
- {triggerItem}
-
-
-
- appendWidget(AlertLevel, 4, 7, 4, 7, 8, 14)} // width and height are passed through here for each widget
- >
- Alert Level
-
- CO2
- IVANS
- Plume Heights
- Alert Level Changes
-
-
-
-
- )
-}
\ No newline at end of file
diff --git a/client/src/components/deprecated/tooltip.jsx b/client/src/components/deprecated/tooltip.jsx
deleted file mode 100644
index ab225cc..0000000
--- a/client/src/components/deprecated/tooltip.jsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react'
-import * as React from 'react'
-
-export const Tooltip = React.forwardRef(function Tooltip(props, ref) {
- const {
- showArrow,
- children,
- disabled,
- portalled = true,
- content,
- contentProps,
- portalRef,
- ...rest
- } = props
-
- if (disabled) return children
-
- return (
-
- {children}
-
-
-
- {showArrow && (
-
-
-
- )}
- {content}
-
-
-
-
- )
-})
diff --git a/client/src/components/layout/Header.jsx b/client/src/components/layout/Header.jsx
index 47adb32..82db471 100644
--- a/client/src/components/layout/Header.jsx
+++ b/client/src/components/layout/Header.jsx
@@ -2,6 +2,7 @@ import { useRecipe, Button, Stack, Text, Flex } from "@chakra-ui/react"
import UserAvatarMenu from "./UserAvatarMenu.jsx";
import { NavLink } from "react-router-dom";
import useAuthStore from "@/store/authStore";
+import {SearchBar} from "@/components/dashboard/SearchBar.jsx";
// Import the asset properly so Vite can bundle it for any server
// import defaultAvatar from "@/assets/user_profile.svg";
@@ -13,25 +14,19 @@ export default function Header() {
const { user } = useAuthStore();
console.log(user);
- const displayName = user ? (user.fullName || user.username) : "Loading...";
+ const displayName = user ? (user.firstName || user.username) : "Loading...";
+ const avatarname = user ? (user.fullName || user.username) : "Loading...";
const displayEmail = user?.email || "LDAP User";
return (
-
- {/* Home Link Logo */}
-
-
+
+
{/* User Profile Section */}
-
- {displayName}
- {displayEmail}
-
+ {displayName}
diff --git a/client/src/components/layout/Sidebar.jsx b/client/src/components/layout/Sidebar.jsx
index 1ca6b77..2ea67ef 100644
--- a/client/src/components/layout/Sidebar.jsx
+++ b/client/src/components/layout/Sidebar.jsx
@@ -1,22 +1,24 @@
-import { useSlotRecipe, Box, Button, IconButton } from "@chakra-ui/react";
-import { ColorModeButton } from "@/components/ui/color-mode.jsx";
-import { useState } from "react";
-import { NavLink } from "react-router-dom";
+import {useSlotRecipe, Box, Button, IconButton} from "@chakra-ui/react";
+import {ColorModeButton} from "@/components/ui/color-mode.jsx";
+import {useState} from "react";
+import {NavLink} from "react-router-dom";
import Tooltip from "../ui/Tooltip.jsx";
-import { Mountain, House, Earth, MapPin, ShieldUser, Menu } from 'lucide-react';
+import {Mountain, House, Earth, MapPin, ShieldUser, Menu, PanelLeftClose, PanelLeftOpen} from 'lucide-react';
const SIDEBAR_DEFAULT_BUTTONS = [
- { icon: House, name: "Home", url: "/home" },
- { icon: Earth, name: "Global Map", url: "/global-map" },
- { icon: MapPin, name: "Regional Map", url: "/regional-map" },
- { icon: Mountain, name: "Volcano", url: "/volcano" },
- { icon: ShieldUser, name: "Admin", url: "/admin" } // TODO remove in the future as default should not have admin
+ {icon: House, name: "Home", url: "/home"},
+ {icon: Earth, name: "Global Map", url: "/global-map"},
+ {icon: MapPin, name: "Regional Map", url: "/regional-map"},
+ {icon: Mountain, name: "Volcano", url: "/volcano"},
+ {icon: ShieldUser, name: "Admin", url: "/admin"} // TODO remove in the future as default should not have admin
];
export default function Sidebar() {
+
const [isOpen, setIsOpen] = useState(true);
- const recipe = useSlotRecipe({ key: "sidebar" });
- const styles = recipe();
+
+ const recipe = useSlotRecipe({key: "sidebar"});
+ const styles = recipe({state: isOpen ? "open" : "closed"});
// TODO we will need to call buttons here by authorized user, for now we update the visible buttons as simply the default buttons
const visibleButtons = SIDEBAR_DEFAULT_BUTTONS;
@@ -24,51 +26,50 @@ export default function Sidebar() {
const toggleSidebar = () => setIsOpen((prev) => !prev);
return (
-
+ {/* Home Link Logo */}
+
+
+
+ {/* Navigation Buttons */}
+ {visibleButtons.map((button) => {
+ const Icon = button.icon;
+ return (
+
+ );
+ })}
+
{/* Menu Trigger */}
-
-
+ {isOpen ? : }
- {/* Navigation Buttons */}
- {visibleButtons.map((button) => {
- const Icon = button.icon;
- return (
-
-
-
- );
- })}
-
- {/* Theme Toggler */}
-
+ {/* Theme Toggler */}
+
+
);
}
\ No newline at end of file
diff --git a/client/src/constants/widgetRegistry.jsx b/client/src/constants/widgetRegistry.jsx
index 3e763da..3ee9d66 100644
--- a/client/src/constants/widgetRegistry.jsx
+++ b/client/src/constants/widgetRegistry.jsx
@@ -78,11 +78,22 @@ export const WIDGET_REGISTRY = {
// The baseline data every new VAA widget starts with
defaultSettings: {
- yAxisScale: 'normal' // Options: 'normal' | 'symlog'
+ yAxisScale: 'log' // Options: 'normal' | 'symlog'
},
allowedSizes: {
medium: { w: 6, h: 12 }
}
},
+ "llm_al_changes_quad_plot": {
+ name: "Alert Level Changes Quad Plot",
+ category: "Computer Science",
+ description: "Percentage of True-Positive, True-Negative, False-Positive, and False-Negative changes in all " +
+ "alert levels.",
+ component: lazy(() => import('../components/Widgets/LlmAlChangesQuadPlot.jsx')),
+ allowedSizes: {
+ small: { w: 3, h: 12 }
+ }
+ }
+
};
\ No newline at end of file
diff --git a/client/src/pages/home/Home.jsx b/client/src/pages/home/Home.jsx
index 8816dbf..4f34457 100644
--- a/client/src/pages/home/Home.jsx
+++ b/client/src/pages/home/Home.jsx
@@ -1,64 +1,69 @@
-import { HStack, Flex, Box } from "@chakra-ui/react";
-import { NavLink, Outlet, useLocation } from "react-router-dom";
-import { SearchBar } from "../../components/dashboard/SearchBar.jsx";
-import { SettingsButton } from "../../components/dashboard/SettingsButton.jsx";
+import {HStack, Flex, Box, useRecipe} from "@chakra-ui/react";
+import {NavLink, Outlet, useLocation} from "react-router-dom";
+import {SearchBar} from "../../components/dashboard/SearchBar.jsx";
+import {SettingsButton} from "../../components/dashboard/SettingsButton.jsx";
+
+// 1. Import your icons (Swap these for whichever you prefer!)
+import {LayoutDashboard, CalendarDays, Cloud, Satellite, Activity} from "lucide-react";
+
+// 2. Define the tabs array
+const HOME_TABS = [
+ {name: "My Dashboard", url: "/home", icon: LayoutDashboard, exact: true},
+ {name: "Daily Activity", url: "/home/daily", icon: CalendarDays},
+ {name: "Gas", url: "/home/gas", icon: Cloud},
+ {name: "Remote Sensing", url: "/home/remote", icon: Satellite},
+ {name: "Seismic", url: "/home/seismic", icon: Activity},
+];
export default function Home() {
+ const recipe = useRecipe({key: "glassTab"});
+ const tabStyles = recipe();
+
const location = useLocation();
const isPersonalDashboard = location.pathname === "/home" || location.pathname === "/home/";
- // 1. CSS making this look like how we want it, TODO turn to theme?
- const tabStyles = {
- fontSize: "lg",
- fontWeight: "normal",
- color: "fg.muted",
- px: 4, // Horizontal padding
- py: 2, // Vertical padding
- borderBottom: "2px solid transparent",
- mb: "-2px", // π₯ The trick: Pulls the active border down to overlap the container's line
- transition: "all 0.2s",
- _hover: { color: "fg" },
- _activeLink: {
- color: "color", // Or whatever your primary text color is
- borderColor: "currentColor", // The active underline
- }
- };
-
return (
-
+
- {/* 2. π οΈ SUB HEADER: Matches your original padding and background */}
-
+
- {/* TABS CONTAINER - Replicates the Tabs.List line container */}
-
- My Dashboard
- Gas
- Seismic
- Remote Sensing
- Daily Activity
-
+ {/* TABS CONTAINER */}
+
+ {HOME_TABS.map((tab) => {
+ const Icon = tab.icon;
+ return (
+
+ {/* flexShrink: 0 ensures the icon never squishes during the gooey animation */}
+
+ {tab.name}
+
+ );
+ })}
- {/* EDIT MODE BUTTONS & CONTROLS */}
-
- {isPersonalDashboard ? (
- <>
-
- {/* This will eventually trigger your Zustand store's edit mode! */}
-
- >
- ) : (
-
- )}
-
+ {/* The Settings button is now inline with the tabs and only renders on the personal dashboard */}
+ {isPersonalDashboard && }
- {/* 3. π οΈ CANVAS AREA */}
-
-
-
+ {/* EDIT MODE BUTTONS & CONTROLS */}
+ {/* Leaving this HStack here in case you plan to put the SearchBar or other global tools back on the right side */}
+
+ {/* Future global controls go here */}
+
+
+
+
+
+
+
);
}
\ No newline at end of file
diff --git a/client/src/pages/home/tabs/MyDashboard.jsx b/client/src/pages/home/tabs/MyDashboard.jsx
index 05f2c24..aed386f 100644
--- a/client/src/pages/home/tabs/MyDashboard.jsx
+++ b/client/src/pages/home/tabs/MyDashboard.jsx
@@ -1,209 +1,195 @@
-import { Box, Text, Flex, IconButton, Spinner, Popover } from "@chakra-ui/react";
-import { X, MoreVertical } from "lucide-react";
-import { Responsive, WidthProvider } from "react-grid-layout";
-import { useDashboardStore } from "@/store/useDashboardStore.jsx";
-import { Suspense, useEffect, useRef } from 'react';
-import { useColorModeValue } from "@/components/ui/color-mode";
+import {Box, Text, Flex, IconButton, Spinner, Popover, useSlotRecipe} from "@chakra-ui/react";
+import {X, MoreVertical} from "lucide-react";
+import {Responsive, WidthProvider} from "react-grid-layout";
+import {useDashboardStore} from "@/store/useDashboardStore.jsx";
+import {Suspense, useEffect, useRef} from 'react';
+
// 1. Import database messenger
import useAuthStore from "@/store/authStore";
// 2. Import registry
-import { WIDGET_REGISTRY } from '@/constants/widgetRegistry.jsx';
+import {WIDGET_REGISTRY} from '@/constants/widgetRegistry.jsx';
// React-Grid-Layout requires this wrapper to automatically calculate screen width
const ResponsiveGridLayout = WidthProvider(Responsive);
export default function MyDashboard() {
- // π Let's adjust for our light and dark mode
- const frameBg = useColorModeValue("gray.100", "gray.800");
- const titleColor = useColorModeValue("gray.800", "gray.100");
- const iconColor = useColorModeValue("gray.600", "gray.400");
- const iconHoverBg = useColorModeValue("gray.100", "gray.700");
- // βοΈ Database Connection
- const { user } = useAuthStore();
+ // βοΈ Database Connection
+ const {user} = useAuthStore();
- // βοΈ Local UI Connections
- const widgets = useDashboardStore(state => state.widgets);
- const isEditing = useDashboardStore(state => state.isEditing);
- const updateLayoutPositions = useDashboardStore(state => state.updateLayoutPositions);
- const removeWidget = useDashboardStore(state => state.removeWidget);
- const setWidgets = useDashboardStore(state => state.setWidgets); // The injection action
+ // βοΈ Local UI Connections
+ const widgets = useDashboardStore(state => state.widgets);
+ const isEditing = useDashboardStore(state => state.isEditing);
+ const updateLayoutPositions = useDashboardStore(state => state.updateLayoutPositions);
+ const removeWidget = useDashboardStore(state => state.removeWidget);
+ const setWidgets = useDashboardStore(state => state.setWidgets); // The injection action
- // π Safety lock to prevent infinite re-renders
- const hasHydrated = useRef(false);
+ // π Safety lock to prevent infinite re-renders
+ const hasHydrated = useRef(false);
- // ==========================================
- // INITIAL LOAD: Grab data from Django
- // ==========================================
- useEffect(() => {
- if (user?.preferences?.dashboard_layout && !hasHydrated.current) {
- const savedLayout = user.preferences.dashboard_layout;
+ // 1. Pull the new global slot recipe
+ const recipe = useSlotRecipe({key: "glassPanel"});
+ const styles = recipe({ layout: "widget" });
+ const popoverStyles = recipe({ layout: "popover" });
- // Safety check: Only inject if it's actually an array of widgets.
- // (Prevents a crash if MariaDB returns a default empty object {})
- if (Array.isArray(savedLayout)) {
- setWidgets(savedLayout);
- }
+ useEffect(() => {
+ if (user?.preferences?.dashboard_layout && !hasHydrated.current) {
+ const savedLayout = user.preferences.dashboard_layout;
- hasHydrated.current = true;
- }
- }, [user, setWidgets]);
+ // Safety check: Only inject if it's actually an array of widgets.
+ // (Prevents a crash if MariaDB returns a default empty object {})
+ if (Array.isArray(savedLayout)) {
+ setWidgets(savedLayout);
+ }
+ hasHydrated.current = true;
+ }
+ }, [user, setWidgets]);
- // π οΈ Format the data for RGL
- const rglLayout = widgets.map(w => ({
- i: w.id,
- x: w.x,
- y: w.y,
- w: w.w,
- h: w.h,
- isResizable: false // π« The magic lock! No resizing allowed globally.
- }));
+ // π οΈ Format the data for RGL
+ const rglLayout = widgets.map(w => ({
+ i: w.id,
+ x: w.x,
+ y: w.y,
+ w: w.w,
+ h: w.h,
+ isResizable: false // π« The magic lock! No resizing allowed globally.
+ }));
- // 3. THE COMPONENT MAPPER
- const renderWidget = (widget) => {
- const registryEntry = WIDGET_REGISTRY[widget.type];
+ // 3. THE COMPONENT MAPPER
+ const renderWidget = (widget) => {
+ const registryEntry = WIDGET_REGISTRY[widget.type];
- if (registryEntry && registryEntry.component) {
- const Component = registryEntry.component;
+ if (registryEntry && registryEntry.component) {
+ const Component = registryEntry.component;
- // 1. Check if this widget type has a settings component registered
- const SettingsUI = registryEntry.settingsComponent;
+ // 1. Check if this widget type has a settings component registered
+ const SettingsUI = registryEntry.settingsComponent;
- return (
- // The "Frame" that holds both the Title and the Chart
-
+ return (
+ <>
+
+
+ {registryEntry.name}
+
- {/* THE TITLE BAR */}
-
-
- {registryEntry.name}
-
+ {/* THE SETTINGS ELLIPSIS */}
+ {SettingsUI ? (
+
+
+
+
+
+
- {/* THE SETTINGS ELLIPSIS */}
- {SettingsUI ? (
-
-
-
-
-
-
-
- {/* NEW: The Positioner lifts the content out of the Flexbox flow */}
-
-
- }>
-
-
-
-
-
- ) : (
-
- )}
-
-
- {/* THE CHART CANVAS */}
-
- {/* Pass the settings object into the actual chart component */}
-
-
-
-
- );
- }
-
- // Fallback if someone asks for a widget that doesn't exist
- return (
-
- Error
- Widget '{widget.type}' not found.
-
- );
- };
-
- return (
-
- updateLayoutPositions(newLayout)} // Fires when user drops a widget
- compactType="vertical" // Auto-packs widgets to the top
- >
- {/* 3. π¨ Paint the Widgets */}
- {widgets.map(widget => (
-
- {/* 4. THE DELETE BUTTON */}
- {isEditing && (
- removeWidget(widget.id)}
- onMouseDown={(e) => e.stopPropagation()} // π« Stops RGL from grabbing the widget
- onTouchStart={(e) => e.stopPropagation()} // π« Same protection for mobile touches
- >
-
-
- )}
-
- {/* 4. SUSPENSE BOUNDARY wrapping the render function */}
-
-
-
- }>
- {renderWidget(widget)}
-
-
-
- ))}
-
-
- {/* Empty State Helper */}
- {widgets.length === 0 && (
-
- Your dashboard is empty. Click Settings to add widgets.
-
+ {/* NEW: The Positioner lifts the content out of the Flexbox flow */}
+
+
+ }>
+
+
+
+
+
+ ) : (
+
)}
-
+
+
+
+
+
+ >
+ );
+ }
+
+ // Fallback if someone asks for a widget that doesn't exist
+ return (
+
+ Error
+ Widget '{widget.type}' not found.
+
);
+ };
+
+ return (
+
+ updateLayoutPositions(newLayout)} // Fires when user drops a widget
+ compactType="vertical" // Auto-packs widgets to the top
+ >
+ {/* 3. π¨ Paint the Widgets */}
+ {widgets.map(widget => (
+
+ {/* 4. THE DELETE BUTTON */}
+ {isEditing && (
+ removeWidget(widget.id)}
+ onMouseDown={(e) => e.stopPropagation()} // π« Stops RGL from grabbing the widget
+ onTouchStart={(e) => e.stopPropagation()} // π« Same protection for mobile touches
+ >
+
+
+ )}
+
+ {/* 4. SUSPENSE BOUNDARY wrapping the render function */}
+
+
+
+ }>
+ {renderWidget(widget)}
+
+
+ ))}
+
+
+ {/* Empty State Helper */}
+ {widgets.length === 0 && (
+
+ Your dashboard is empty. Click Settings to add widgets.
+
+ )}
+
+ );
}
\ No newline at end of file
diff --git a/client/src/theme.js b/client/src/theme.js
index c0200ba..41e836d 100644
--- a/client/src/theme.js
+++ b/client/src/theme.js
@@ -1,14 +1,12 @@
-import { defineSlotRecipe, createSystem, defaultConfig, defineConfig, defineRecipe } from "@chakra-ui/react";
+import {defineSlotRecipe, createSystem, defaultConfig, defineConfig, defineRecipe} from "@chakra-ui/react";
// RECIPES
const headerRecipe = defineRecipe({
base: {
- bg: "{base200}",
+ bg: "{transparent}",
color: "text",
width: "100%",
height: "100%",
- borderBottom: "1px solid",
- borderBottomColor:"{border}",
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
@@ -16,48 +14,177 @@ const headerRecipe = defineRecipe({
}
});
+const sidebarRecipe = defineSlotRecipe({
+ slots: ["root", "logo", "footer", "button_text","buttons", "collapse_button"],
+ base: {
+ root: {
+ // 1. Semi-transparent backgrounds
+ bg: {
+ base: "linear-gradient(to bottom right, #99000040 20%, #FF660040 60%, #EEEE0040 100%)",
+ _dark: "linear-gradient(to bottom right, #9900003C 20%, #FF66003C 60%, #FFC0003C 100%)"
+ },
+
+ // 2. The glass blur effect
+ backdropFilter: "blur(12px)",
+ WebkitBackdropFilter: "blur(12px)",
+
+ // 3. Subtle semi-transparent borders
+ border: "1px solid",
+ borderColor: {base: "rgba(255, 255, 255, 0.8)", _dark: "rgba(255, 255, 255, 0.1)"},
+
+ // CRITICAL: Remove the right border so it doesn't draw a line through your cutout
+ 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",
+ display: "flex",
+ flexDirection: "column",
+ position: "relative",
+ justifyContent: "start",
+ alignItems: "stretch",
+ gap: 4,
+ transition: "width 0.4s ease",
+
+ // CRITICAL: Adjust padding and overflow to allow the button to touch the right edge
+ pl: 2, // Keep left padding
+ py: 2, // Keep top/bottom padding
+ pr: 0, // Remove right padding
+ pt: 4, // Added top padding to give the logo room to breathe
+ overflow: "visible", // Allows the curved corner patches to render outside the button
+ zIndex: 10,
+ },
+ buttons: {
+ position: "relative", // Required to anchor the corner patches
+ bg: "transparent",
+
+ justifyContent: "flex-start",
+ transition: "background-color 0.4s ease, color 0.4s ease",
+
+ "& svg": {
+ transition: "stroke-width 0.4s ease",
+ flexShrink: "0"
+ },
+
+ _hover: {
+ bg: "rgba(255, 255, 255, 0.1)",
+ color: "{text}",
+ fontWeight: "semibold",
+ "& svg": {
+ strokeWidth: "3"
+ }
+ },
+
+ fontSize: "l",
+ color: {base: "{text}"},
+ variant: "ghost",
+ width: "100%",
+
+ // Flat on the right, rounded on the left
+ borderRightRadius: "0",
+ borderLeftRadius: "4xl",
+
+ "&.active": {
+ bg: "{MainBackground}",
+ color: "{textInverted}",
+
+ _hover: {
+ bg: "{MainBackground}",
+ color: "{textInverted}",
+ fontWeight: "normal",
+ "& svg": {
+ strokeWidth: "2" // Keeps the active icon at normal thickness on hover
+ }
+ },
+
+ // The Top Inverted Curve
+ _before: {
+ content: '""',
+ position: "absolute",
+ right: "-1px",
+ top: "-25px",
+ width: "25px",
+ height: "25px",
+ bg: "inherit",
+ WebkitMaskImage: "radial-gradient(circle at top left, transparent 25px, black 25.5px)",
+ maskImage: "radial-gradient(circle at top left, transparent 25px, black 25.5px)",
+ pointerEvents: "none",
+ },
+
+ // The Bottom Inverted Curve
+ _after: {
+ content: '""',
+ position: "absolute",
+ right: "-1px",
+ bottom: "-25px",
+ width: "25px",
+ height: "25px",
+ bg: "inherit",
+ 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: {
+ width: "100%",
+ height: "auto",
+ overflow: "hidden",
+ fontWeight: "bold",
+ px: 0,
+ },
+ footer: {
+ mt: "auto",
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ pb: 4,
+ pr: 2,
+ transition: "opacity 0.3s ease, visibility 0.3s ease, margin 0.3s ease"
+ },
+ button_text: {
+ transition: "opacity 0.3s ease, visibility 0.3s ease, margin 0.3s ease",
+ whiteSpace: "nowrap"
+ },
+ collapse_button: {
+ "& svg": { flexShrink: "0" },
+ },
+ },
+ variants: {
+ state: {
+ open: {
+ root: { width: "190px" },
+ logo: { justifyContent: "flex-start", ml: 2, textStyle: "4xl" },
+ footer: { gap: 16, flexDirection: "row" },
+ button_text: {ml: 3, opacity: 1, visibility: "visible"},
+ },
+ closed: {
+ root: { width: "71px" },
+ logo: { justifyContent: "center", ml: -1, textStyle: "3xl" },
+ footer: { gap: 2, flexDirection: "column-reverse" },
+ button_text: {ml: 0, opacity: 0, visibility: "hidden"},
+ }
+ }
+ }
+});
+
const dashboardRecipe = defineRecipe({
base: {
- bg: "{base200}"
+ bg: "{MainBackground}"
}
});
const canvasRecipe = defineRecipe({
base: {
- bg: "{base200}",
+ bg: "{transparent}",
zIndex: "-1",
color: "{text}"
}
});
-const sidebarRecipe = defineSlotRecipe({
- slots: ["root", "close_button", "buttons", "dark_mode_button"],
- base: {
- root: {
- bg: "{base200}",
- borderRight: "1px solid",
- borderColor: "{border}",
- width: "100%",
- height: "100%",
- display: "flex",
- flexDirection: "column",
- justifyContent: "start",
- alignItems: "start",
- gap: 2,
- p: 2,
- },
- buttons: {
- bg: "transparent",
- _hover: {bg: "{primaryLt}", color: "{textInverted}"},
- "&.active": {bg: "{primary}", color: "{textInverted}"},
- fontSize: "xl",
- color: {base: "{text}"},
- variant: "ghost",
- width: "100%",
- },
- }
-});
-
const widgetRecipe = defineSlotRecipe({
slots: ["root", "header", "canvas"],
base: {
@@ -96,19 +223,144 @@ const widgetRecipe = defineSlotRecipe({
}
});
+const glassPanelRecipe = defineSlotRecipe({
+ slots: ["root", "header", "body"],
+
+ // π§ THE GLASS MATH (Applies to everything)
+ base: {
+ root: {
+ display: "flex",
+ flexDirection: "column",
+ position: "relative",
+ 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},
+ 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: 4,
+ boxShadow: "xl",
+ minW: "240px",
+ // 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)",
+ }
+ }
+ },
+ mode: {
+ editing: {
+ root: {
+ boxShadow: "outline",
+ borderStyle: "dashed",
+ borderWidth: "2px",
+ borderColor: "orange.300",
+ cursor: "grab",
+ }
+ }
+ }
+ },
+
+ defaultVariants: {
+ layout: "widget"
+ }
+});
+
+const glassTabRecipe = defineRecipe({
+ base: {
+ display: "flex",
+ alignItems: "center",
+ height: "40px",
+
+ // The Circle State (Inactive)
+ maxWidth: "40px",
+ minWidth: "40px",
+ px: "8px",
+ gap: 2,
+
+ // CRITICAL: Locks the shape to a perfect circle/pill
+ borderRadius: "full",
+
+ overflow: "hidden",
+ whiteSpace: "nowrap",
+ textDecoration: "none",
+ fontWeight: "bold",
+ color: "fg.muted",
+
+ // --- THE GLASS EFFECT (Inactive) ---
+ // Uses a highly transparent white/black depending on mode
+ bg: {base: "rgba(255, 255, 255, 0.4)", _dark: "rgba(255, 255, 255, 0.03)"},
+ backdropFilter: "blur(12px)",
+ WebkitBackdropFilter: "blur(12px)",
+ border: "1px solid",
+ borderColor: {base: "rgba(0, 0, 0, 0.05)", _dark: "rgba(255, 255, 255, 0.08)"},
+ boxShadow: "0 4px 6px rgba(0, 0, 0, 0.02)", // Tiny shadow to lift it off the canvas
+
+ transition: "all 0.85s cubic-bezier(0.34, 1.56, 0.64, 1)",
+
+ "&:hover, &[data-state='open'], &[aria-expanded='true']": {
+ maxWidth: "250px",
+ pr: "16px",
+ // --- THE GLASS EFFECT (Hover) ---
+ // Becomes slightly more opaque when you mouse over
+ bg: {base: "rgba(255, 255, 255, 0.8)", _dark: "rgba(255, 255, 255, 0.1)"},
+ borderColor: {base: "rgba(0, 0, 0, 0.1)", _dark: "rgba(255, 255, 255, 0.15)"},
+ color: "fg",
+ textDecoration: "none",
+ },
+
+ "&.active": {
+ maxWidth: "250px",
+ pr: "16px",
+
+ bg: "{gooeyButtonBackground}",
+ borderColor: "transparent",
+ color: "{textInverted}",
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)", // Stronger shadow for the active tab
+
+ _hover: {
+ bg: "{gooeyButtonBackground}",
+ cursor: "default",
+ textDecoration: "none",
+ }
+ }
+ }
+});
+
const config = defineConfig({
globalCss: {
".react-grid-placeholder": {
- background: "#c0d2e3ff !important",
- borderColor: "#a9b6c2ff !important",
- boxShadow: "0 0 10px #c0d2e3ff !important",
- opacity: "1 !important",
- borderRadius: "5px",
- _dark: {
- background: "#393c3fff !important",
- borderColor: "#000000ff !important",
- boxShadow: "0 0 10px #16181bff !important",
+ background: "#c0d2e3ff !important",
+ borderColor: "#a9b6c2ff !important",
+ boxShadow: "0 0 10px #c0d2e3ff !important",
opacity: "1 !important",
+ borderRadius: "5px",
+ _dark: {
+ background: "#393c3fff !important",
+ borderColor: "#000000ff !important",
+ boxShadow: "0 0 10px #16181bff !important",
+ opacity: "1 !important",
},
},
".react-resizable-handle-se": {
@@ -171,31 +423,38 @@ 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"}},
+ 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: "lightgrey"}},
- textInverted: {value: "white"},
- icon: {value: {base: "#000000", _dark: "lightgrey"}},
+ text: {value: {base: "#000000", _dark: "#FFFFFF"}},
+ textInverted: {value: {base: "#000000", _dark: "#FFFFFF"}},
+ icon: {value: {base: "#000000", _dark: "#FFFFFF"}},
+ 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"}},
},
},
tokens: {
fonts: {
- body: { value: "system-ui, sans-serif" },
+ body: {value: "system-ui, sans-serif"},
},
},
recipes: {
header: headerRecipe,
dashboard: dashboardRecipe,
canvas: canvasRecipe,
+ glassTab: glassTabRecipe
},
slotRecipes: {
sidebar: sidebarRecipe,
widget: widgetRecipe,
+ glassPanel: glassPanelRecipe,
},
},
});