2026-07-29 13:48:38 -07:00
|
|
|
import {useState, useMemo, useRef} from "react";
|
2026-06-09 14:06:01 -07:00
|
|
|
import {
|
2026-07-29 13:48:38 -07:00
|
|
|
Button,
|
|
|
|
|
Box,
|
|
|
|
|
Text,
|
|
|
|
|
Flex,
|
|
|
|
|
Grid,
|
|
|
|
|
Dialog,
|
|
|
|
|
Tabs,
|
|
|
|
|
Badge,
|
|
|
|
|
Menu,
|
|
|
|
|
HStack,
|
|
|
|
|
useRecipe, useSlotRecipe
|
2026-06-09 14:06:01 -07:00
|
|
|
} from "@chakra-ui/react";
|
2026-07-29 13:48:38 -07:00
|
|
|
import {Settings, Plus, LayoutGrid, Move, Save, X} from 'lucide-react';
|
|
|
|
|
import {useDashboardStore} from "../../store/useDashboardStore.jsx";
|
|
|
|
|
import {WIDGET_REGISTRY} from "../../constants/widgetRegistry.jsx";
|
2026-06-09 14:06:01 -07:00
|
|
|
|
|
|
|
|
export function SettingsButton() {
|
2026-07-29 13:48:38 -07:00
|
|
|
// 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);
|
2026-07-09 15:10:50 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
const recipe = useRecipe({key: "glassTab"});
|
|
|
|
|
const tabStyles = recipe();
|
2026-07-09 15:10:50 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
// Pull the glass container styles for the dropdown
|
|
|
|
|
const panelRecipe = useSlotRecipe({key: "glassPanel"});
|
|
|
|
|
const panelStyles = panelRecipe({ layout: "dropdown" });
|
2026-06-09 14:06:01 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
const [isLibraryOpen, setIsLibraryOpen] = useState(false);
|
2026-06-09 14:06:01 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
// Smart Add Handler
|
|
|
|
|
const handleAddWidget = (widgetId, sizeKey) => {
|
|
|
|
|
enterEditMode();
|
|
|
|
|
addWidget(widgetId, sizeKey);
|
|
|
|
|
};
|
2026-07-09 15:10:50 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
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;
|
|
|
|
|
}, {});
|
|
|
|
|
}, []);
|
2026-06-09 14:06:01 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
const categories = Object.keys(categorizedWidgets);
|
2026-06-09 14:06:01 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
// 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();
|
|
|
|
|
};
|
2026-07-22 08:46:58 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{/* THE STATIC DROPDOWN MENU */}
|
|
|
|
|
<Menu.Root positioning={{getAnchorRect}}>
|
|
|
|
|
<Menu.Trigger asChild>
|
|
|
|
|
<Box
|
|
|
|
|
ref={menuRef}
|
|
|
|
|
as="button"
|
|
|
|
|
css={tabStyles}
|
|
|
|
|
>
|
|
|
|
|
{/* size increased to 24 to match tabs. flexShrink: 0 is CRITICAL */}
|
|
|
|
|
<Settings size={24} style={{flexShrink: 0}}/>
|
2026-06-09 14:06:01 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
{/* Dynamic text based on editing state! */}
|
|
|
|
|
<Box as="span">
|
|
|
|
|
{isEditing ? "Editing Layout..." : "Dashboard Settings"}
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
</Menu.Trigger>
|
2026-07-09 15:10:50 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
<Menu.Positioner>
|
|
|
|
|
<Menu.Content css={panelStyles.root} zIndex="dropdown" minW="200px">
|
|
|
|
|
{isEditing ? (
|
|
|
|
|
<>
|
|
|
|
|
<Menu.Item onClick={saveEdit} color="green.600" cursor="pointer" _hover={{fontWeight: "semibold"}}>
|
|
|
|
|
<Save size={16} style={{marginRight: '8px'}}/> Save Layout
|
|
|
|
|
</Menu.Item>
|
2026-07-22 08:46:58 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
<Menu.Item onClick={cancelEdit} color="red.500" cursor="pointer" _hover={{fontWeight: "semibold"}}>
|
|
|
|
|
<X size={16} style={{marginRight: '8px'}}/> Discard Changes
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<Menu.Item onClick={enterEditMode} cursor="pointer" _hover={{fontWeight: "semibold"}}>
|
|
|
|
|
<Move size={16} style={{marginRight: '8px'}}/> Edit Layout
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
)}
|
2026-06-09 14:06:01 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
<Menu.Separator my={1} borderColor="glassSeperator"/>
|
2026-06-09 14:06:01 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
<Menu.Item onClick={() => setIsLibraryOpen(true)} cursor="pointer" _hover={{fontWeight: "semibold"}}>
|
|
|
|
|
<LayoutGrid size={16} style={{marginRight: '8px'}}/> Widget Library
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
</Menu.Content>
|
|
|
|
|
</Menu.Positioner>
|
|
|
|
|
</Menu.Root>
|
2026-06-09 14:06:01 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
{/* THE WIDGET LIBRARY MODAL (Controlled by isLibraryOpen) */}
|
|
|
|
|
<Dialog.Root open={isLibraryOpen} onOpenChange={(e) => setIsLibraryOpen(e.open)} size="xl" placement="center">
|
|
|
|
|
<Dialog.Backdrop bg="blackAlpha.600"/>
|
|
|
|
|
<Dialog.Positioner>
|
|
|
|
|
<Dialog.Content bg="gray.50" borderRadius="xl" overflow="hidden" boxShadow="xl" maxW="800px">
|
|
|
|
|
<Dialog.Header bg="gray.800" color="white" py={4}>
|
|
|
|
|
<Dialog.Title fontSize="xl" fontWeight="bold">Add Dashboard Widgets</Dialog.Title>
|
|
|
|
|
</Dialog.Header>
|
2026-06-09 14:06:01 -07:00
|
|
|
|
2026-07-29 13:48:38 -07:00
|
|
|
<Dialog.Body p={0}>
|
|
|
|
|
<Tabs.Root defaultValue={categories[0]} variant="enclosed" colorScheme="blue">
|
|
|
|
|
<Tabs.List bg="white" px={4} pt={4} borderBottom="1px solid" borderColor="gray.200">
|
|
|
|
|
{categories.map(cat => (
|
|
|
|
|
<Tabs.Trigger key={cat} value={cat} pb={2}>{cat}</Tabs.Trigger>
|
|
|
|
|
))}
|
|
|
|
|
</Tabs.List>
|
|
|
|
|
|
|
|
|
|
{categories.map(cat => (
|
|
|
|
|
<Tabs.Content key={cat} value={cat} p={6}>
|
|
|
|
|
<Grid templateColumns="repeat(auto-fill, minmax(300px, 1fr))" gap={4}>
|
|
|
|
|
{categorizedWidgets[cat].map(widget => (
|
|
|
|
|
<Flex key={widget.id} direction="column" bg="white" p={4} borderRadius="md" border="1px solid"
|
|
|
|
|
borderColor="gray.200" boxShadow="sm">
|
|
|
|
|
<Text fontWeight="bold" fontSize="md" mb={1}>{widget.name}</Text>
|
|
|
|
|
<Text fontSize="sm" color="gray.500" mb={4} flex="1">{widget.description}</Text>
|
|
|
|
|
|
|
|
|
|
<Box borderTop="1px solid" borderColor="gray.100" pt={3}>
|
|
|
|
|
<Text fontSize="xs" fontWeight="bold" color="gray.400" textTransform="uppercase" mb={2}>Add
|
|
|
|
|
to Grid:</Text>
|
|
|
|
|
<HStack gap={2} flexWrap="wrap">
|
|
|
|
|
{Object.keys(widget.allowedSizes).map(sizeKey => (
|
|
|
|
|
<Badge
|
|
|
|
|
key={sizeKey}
|
|
|
|
|
as="button"
|
|
|
|
|
onClick={() => 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"
|
|
|
|
|
>
|
|
|
|
|
<Plus size={12} style={{display: 'inline', marginRight: '4px'}}/>
|
|
|
|
|
{sizeKey.charAt(0).toUpperCase() + sizeKey.slice(1)}
|
|
|
|
|
</Badge>
|
|
|
|
|
))}
|
|
|
|
|
</HStack>
|
|
|
|
|
</Box>
|
|
|
|
|
</Flex>
|
|
|
|
|
))}
|
|
|
|
|
</Grid>
|
|
|
|
|
</Tabs.Content>
|
|
|
|
|
))}
|
|
|
|
|
</Tabs.Root>
|
|
|
|
|
</Dialog.Body>
|
|
|
|
|
<Dialog.CloseTrigger position="absolute" top={3} right={3} color="white"/>
|
|
|
|
|
</Dialog.Content>
|
|
|
|
|
</Dialog.Positioner>
|
|
|
|
|
</Dialog.Root>
|
|
|
|
|
</>
|
|
|
|
|
);
|
2026-06-09 14:06:01 -07:00
|
|
|
}
|