diff --git a/src/custom/DashboardLayout/index.tsx b/src/custom/DashboardLayout/index.tsx new file mode 100644 index 000000000..d5806b18a --- /dev/null +++ b/src/custom/DashboardLayout/index.tsx @@ -0,0 +1,137 @@ +import React, { useState, useEffect } from 'react'; +import { Box } from '../../base'; +import { useTheme, useMediaQuery } from '../../theme'; +import { SwipeableDrawer } from '@mui/material'; + +export interface DashboardLayoutProps { + /** The main dashboard content (typically the React-Grid-Layout) */ + children: React.ReactNode; + + /** Whether the right-hand sidebar should be visible */ + isSidebarOpen: boolean; + + /** The content to render inside the sidebar (e.g., Widget Gallery) */ + sidebarContent: React.ReactNode; + + /** Optional custom width for the sidebar. Defaults to responsive width. */ + sidebarWidth?: string | number | Partial>; + + /** Optional sticky top offset for the sidebar (useful if page has a top navbar) */ + sidebarTopOffset?: string | number; + + /** Optional fixed height for the sticky sidebar. Defaults to 100vh */ + sidebarHeight?: string | number; + + +} + +export const DashboardLayout: React.FC = ({ + children, + isSidebarOpen, + sidebarContent, + sidebarWidth = { xs: '100%', md: '350px' }, + sidebarTopOffset = '0', + sidebarHeight = '100vh', +}) => { + const theme = useTheme(); + // We use the 'md' breakpoint (900px default) to switch between mobile and desktop layout + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + + const [isMobileDrawerOpen, setIsMobileDrawerOpen] = useState(false); + const drawerBleeding = 56; + + useEffect(() => { + if (isSidebarOpen) { + setIsMobileDrawerOpen(true); + } else { + setIsMobileDrawerOpen(false); + } + }, [isSidebarOpen]); + + return ( + + + {children} + + + {isMobile && ( + <> + { + setIsMobileDrawerOpen(false); + }} + onOpen={() => setIsMobileDrawerOpen(true)} + swipeAreaWidth={isSidebarOpen ? drawerBleeding : 0} + disableSwipeToOpen={false} + ModalProps={{ + keepMounted: true, + }} + sx={{ + '& .MuiPaper-root': { + height: `calc(50% - ${drawerBleeding}px)`, + overflow: 'visible', + }, + '& .MuiDrawer-paper': { + borderTopLeftRadius: '16px', + borderTopRightRadius: '16px', + }, + }} + > + { + const nextState = !isMobileDrawerOpen; + setIsMobileDrawerOpen(nextState); + }} + > + + + + {sidebarContent} + + + + )} + + {isSidebarOpen && !isMobile && ( + + {sidebarContent} + + )} + + ); +}; diff --git a/src/custom/WidgetPicker/WidgetPicker.tsx b/src/custom/WidgetPicker/WidgetPicker.tsx new file mode 100644 index 000000000..69483f37e --- /dev/null +++ b/src/custom/WidgetPicker/WidgetPicker.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import { Box, IconButton, Stack, Typography } from '../../base'; +import { AddIcon, CloseIcon } from '../../icons'; +import { useTheme } from '../../theme'; +import { SxProps, Theme } from '@mui/material'; + +export interface WidgetItem { + key: string; + title: string; + thumbnail?: string; + [key: string]: unknown; // Allow passing extra widget properties +} + +export interface WidgetPickerProps { + /** The list of widgets available to add */ + widgetsToAdd: WidgetItem[]; + + /** Callback when a widget is clicked to be added */ + onAddWidget: (widget: Omit, key: string) => void; + + /** Optional callback to close the picker (renders a Close icon if provided) */ + onClose?: () => void; + + /** Custom background color for the header. Defaults to theme.palette.background.default */ + headerBackgroundColor?: string; + + /** Custom text color for the header. Defaults to theme.palette.text.primary */ + headerTextColor?: string; + + /** Custom styles for the outer container (e.g. for custom box shadows or borders) */ + containerSx?: SxProps; +} + +export const WidgetPicker: React.FC = ({ + widgetsToAdd, + onAddWidget, + onClose, + headerBackgroundColor, + headerTextColor, + containerSx = {}, +}) => { + const theme = useTheme(); + + return ( + + + + Widgets + + {onClose && ( + + + + )} + + + + {widgetsToAdd.length === 0 && ( + + All widgets added to the layout. + + )} + + {widgetsToAdd.map(({ key, ...widget }) => ( + + + {widget.title} + onAddWidget(widget, key)} + > + + + + {widget.thumbnail && ( + {widget.title} + )} + + ))} + + + ); +}; diff --git a/src/custom/WidgetPicker/index.tsx b/src/custom/WidgetPicker/index.tsx new file mode 100644 index 000000000..075b1fead --- /dev/null +++ b/src/custom/WidgetPicker/index.tsx @@ -0,0 +1 @@ +export * from './WidgetPicker'; diff --git a/src/custom/index.ts b/src/custom/index.ts index 3f35c351e..8de85c68e 100644 --- a/src/custom/index.ts +++ b/src/custom/index.ts @@ -1,7 +1,9 @@ // Export all custom components export * from './CustomTooltip'; +export * from './DashboardLayout'; export * from './HelperTextPopover'; export * from './Markdown'; export * from './Modal'; export * from './RJSFFormWrapper'; export * from './StyledAccordion'; +export * from './WidgetPicker'; diff --git a/src/custom/index.tsx b/src/custom/index.tsx index dd445847f..b41afd0ed 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -189,3 +189,4 @@ export * from './RJSFFormWrapper'; export * from './ShareModal'; export * from './UserSearchField'; export * from './Workspaces'; +export * from './WidgetPicker'; diff --git a/src/index.tsx b/src/index.tsx index 5eff2e6ab..c5f9ffa3d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -32,6 +32,11 @@ export { type DangerConfirmationCheckbox, type DangerConfirmationModalProps } from './custom/DangerConfirmationModal'; + +export { + DashboardLayout, + type DashboardLayoutProps +} from './custom/DashboardLayout'; // Same nested-barrel dts-drop quirk as FeedbackButton above: UniversalFilter // (and its FilterColumn / UniversalFilterProps types) reaches the entry only // through `export * from './custom'`, so rollup-plugin-dts drops it from the @@ -59,3 +64,9 @@ export { type PermissionShieldProps, type PermissionUserContext } from './custom/permissions'; + +export { + WidgetPicker, + type WidgetPickerProps, + type WidgetItem +} from './custom/WidgetPicker';