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, }, }, });