From 8ce6b105f3271b90dc4e45f544ee1d469f5b2de9 Mon Sep 17 00:00:00 2001 From: NSTKrishna Date: Mon, 13 Jul 2026 06:45:37 +0530 Subject: [PATCH 01/10] Add DashboardLayout and WidgetPicker components Introduce a responsive DashboardLayout (sticky sidebar on desktop, bottom SwipeableDrawer on mobile) and a WidgetPicker UI with types for WidgetItem. Added files: src/custom/DashboardLayout/index.tsx, src/custom/WidgetPicker/WidgetPicker.tsx, src/custom/WidgetPicker/index.tsx. Updated exports in src/custom/index.ts, src/custom/index.tsx and root src/index.tsx to re-export the new components and their props/types so they are available from the package API. Signed-off-by: NSTKrishna --- src/custom/DashboardLayout/index.tsx | 134 +++++++++++++++++++ src/custom/WidgetPicker/WidgetPicker.tsx | 157 +++++++++++++++++++++++ src/custom/WidgetPicker/index.tsx | 1 + src/custom/index.ts | 2 + src/custom/index.tsx | 1 + src/index.tsx | 11 ++ 6 files changed, 306 insertions(+) create mode 100644 src/custom/DashboardLayout/index.tsx create mode 100644 src/custom/WidgetPicker/WidgetPicker.tsx create mode 100644 src/custom/WidgetPicker/index.tsx diff --git a/src/custom/DashboardLayout/index.tsx b/src/custom/DashboardLayout/index.tsx new file mode 100644 index 000000000..1044393b5 --- /dev/null +++ b/src/custom/DashboardLayout/index.tsx @@ -0,0 +1,134 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Drawer } 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; + + /** Callback fired when the component requests to be closed (e.g. clicking the backdrop on mobile) */ + onClose?: () => void; +} + +export const DashboardLayout: React.FC = ({ + children, + isSidebarOpen, + sidebarContent, + sidebarWidth = { xs: '100%', md: '350px' }, + sidebarTopOffset = '0', + sidebarHeight = '100vh', + onClose, +}) => { + 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} + + + {isSidebarOpen && isMobile && ( + <> + setIsMobileDrawerOpen(false)} + onOpen={() => setIsMobileDrawerOpen(true)} + swipeAreaWidth={drawerBleeding} + disableSwipeToOpen={false} + ModalProps={{ + keepMounted: true, + }} + sx={{ + '& .MuiPaper-root': { + height: `calc(50% - ${drawerBleeding}px)`, + overflow: 'visible', + }, + '& .MuiDrawer-paper': { + borderTopLeftRadius: '16px', + borderTopRightRadius: '16px', + }, + }} + > + setIsMobileDrawerOpen(!isMobileDrawerOpen)} + > + + + + {sidebarContent} + + + + )} + + {isSidebarOpen && !isMobile && ( + + {sidebarContent} + + )} + + ); +}; diff --git a/src/custom/WidgetPicker/WidgetPicker.tsx b/src/custom/WidgetPicker/WidgetPicker.tsx new file mode 100644 index 000000000..6c83ddb37 --- /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]: any; // 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: any, 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.card */ + 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 608c8f233..ccf81fe0a 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -186,3 +186,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 1129de512..84da48173 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 @@ -51,3 +56,9 @@ export { type Key, type PermissionShieldProps } from './custom/permissions'; + +export { + WidgetPicker, + type WidgetPickerProps, + type WidgetItem +} from './custom/WidgetPicker'; From c1fd1f8aa2776130a0adb9fc135a1a58b6722229 Mon Sep 17 00:00:00 2001 From: NSTKrishna Date: Mon, 13 Jul 2026 06:59:24 +0530 Subject: [PATCH 02/10] Call onClose on drawer close; tighten WidgetPicker types Invoke the optional onClose callback when the mobile drawer is closed (in SwipeableDrawer.onClose and the header toggle). Remove an unused Drawer import. Improve WidgetPicker typings: use unknown for the extra properties index signature and change onAddWidget to accept the widget payload without its 'key'. Also update the headerBackgroundColor JSDoc default. These changes ensure proper close propagation and stronger type safety. Signed-off-by: NSTKrishna --- src/custom/DashboardLayout/index.tsx | 15 ++++++++++++--- src/custom/WidgetPicker/WidgetPicker.tsx | 6 +++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/custom/DashboardLayout/index.tsx b/src/custom/DashboardLayout/index.tsx index 1044393b5..4558625d7 100644 --- a/src/custom/DashboardLayout/index.tsx +++ b/src/custom/DashboardLayout/index.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Box, Drawer } from '../../base'; +import { Box } from '../../base'; import { useTheme, useMediaQuery } from '../../theme'; import { SwipeableDrawer } from '@mui/material'; @@ -61,7 +61,10 @@ export const DashboardLayout: React.FC = ({ setIsMobileDrawerOpen(false)} + onClose={() => { + setIsMobileDrawerOpen(false); + onClose?.(); + }} onOpen={() => setIsMobileDrawerOpen(true)} swipeAreaWidth={drawerBleeding} disableSwipeToOpen={false} @@ -96,7 +99,13 @@ export const DashboardLayout: React.FC = ({ borderBottom: `1px solid ${theme.palette.divider}`, cursor: 'pointer', }} - onClick={() => setIsMobileDrawerOpen(!isMobileDrawerOpen)} + onClick={() => { + const nextState = !isMobileDrawerOpen; + setIsMobileDrawerOpen(nextState); + if (!nextState) { + onClose?.(); + } + }} > void; + 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.card */ + /** 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 */ From 7d0b48d232b67fd1c5df4ee6aee6f5c6c42c4cdc Mon Sep 17 00:00:00 2001 From: NSTKrishna Date: Mon, 13 Jul 2026 08:19:19 +0530 Subject: [PATCH 03/10] Fix mobile drawer visibility Remove the isSidebarOpen condition from the mobile SwipeableDrawer to ensure the drawer is always displayed on mobile devices, regardless of sidebar state. Signed-off-by: NSTKrishna --- src/custom/DashboardLayout/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/custom/DashboardLayout/index.tsx b/src/custom/DashboardLayout/index.tsx index 4558625d7..1fd87e52d 100644 --- a/src/custom/DashboardLayout/index.tsx +++ b/src/custom/DashboardLayout/index.tsx @@ -56,7 +56,7 @@ export const DashboardLayout: React.FC = ({ {children} - {isSidebarOpen && isMobile && ( + {isMobile && ( <> Date: Mon, 13 Jul 2026 09:38:51 +0530 Subject: [PATCH 04/10] Fix mobile dashboard drawer behavior Only expose the swipe area when the sidebar is open, and stop firing the layout `onClose` callback from mobile drawer toggles. This keeps mobile drawer state changes local while preserving the wider sidebar interaction area when appropriate. Signed-off-by: NSTKrishna --- src/custom/DashboardLayout/index.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/custom/DashboardLayout/index.tsx b/src/custom/DashboardLayout/index.tsx index 1fd87e52d..ed25df8d2 100644 --- a/src/custom/DashboardLayout/index.tsx +++ b/src/custom/DashboardLayout/index.tsx @@ -63,10 +63,9 @@ export const DashboardLayout: React.FC = ({ open={isMobileDrawerOpen} onClose={() => { setIsMobileDrawerOpen(false); - onClose?.(); }} onOpen={() => setIsMobileDrawerOpen(true)} - swipeAreaWidth={drawerBleeding} + swipeAreaWidth={isSidebarOpen ? drawerBleeding : 0} disableSwipeToOpen={false} ModalProps={{ keepMounted: true, @@ -102,9 +101,6 @@ export const DashboardLayout: React.FC = ({ onClick={() => { const nextState = !isMobileDrawerOpen; setIsMobileDrawerOpen(nextState); - if (!nextState) { - onClose?.(); - } }} > Date: Mon, 13 Jul 2026 09:45:31 +0530 Subject: [PATCH 05/10] Remove DashboardLayout onClose prop Drops the unused `onClose` prop from `DashboardLayout`'s public props and implementation, simplifying the component API. Signed-off-by: NSTKrishna --- src/custom/DashboardLayout/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/custom/DashboardLayout/index.tsx b/src/custom/DashboardLayout/index.tsx index ed25df8d2..d5806b18a 100644 --- a/src/custom/DashboardLayout/index.tsx +++ b/src/custom/DashboardLayout/index.tsx @@ -22,8 +22,7 @@ export interface DashboardLayoutProps { /** Optional fixed height for the sticky sidebar. Defaults to 100vh */ sidebarHeight?: string | number; - /** Callback fired when the component requests to be closed (e.g. clicking the backdrop on mobile) */ - onClose?: () => void; + } export const DashboardLayout: React.FC = ({ @@ -33,7 +32,6 @@ export const DashboardLayout: React.FC = ({ sidebarWidth = { xs: '100%', md: '350px' }, sidebarTopOffset = '0', sidebarHeight = '100vh', - onClose, }) => { const theme = useTheme(); // We use the 'md' breakpoint (900px default) to switch between mobile and desktop layout From d24fc97ac35c143308f0190390498a6140af82b5 Mon Sep 17 00:00:00 2001 From: NSTKrishna Date: Tue, 28 Jul 2026 01:46:09 +0530 Subject: [PATCH 06/10] Add WidgetEmptyState and tidy exports Introduces a new `WidgetEmptyState` component with optional icon and action button support, and exports it through both `custom` and root barrels. Refactors `DashboardLayout` into its own file with explicit type/value re-exports and updates mobile sidebar rendering so the drawer is only mounted when the sidebar is open. Also switches `WidgetPicker` to explicit named/type exports for a clearer public API surface. Signed-off-by: NSTKrishna --- .../DashboardLayout/DashboardLayout.tsx | 134 +++++++++++++++++ src/custom/DashboardLayout/index.tsx | 139 +----------------- src/custom/WidgetEmptyState/index.tsx | 81 ++++++++++ src/custom/WidgetPicker/index.tsx | 2 +- src/custom/index.ts | 1 + src/index.tsx | 5 + 6 files changed, 224 insertions(+), 138 deletions(-) create mode 100644 src/custom/DashboardLayout/DashboardLayout.tsx create mode 100644 src/custom/WidgetEmptyState/index.tsx diff --git a/src/custom/DashboardLayout/DashboardLayout.tsx b/src/custom/DashboardLayout/DashboardLayout.tsx new file mode 100644 index 000000000..721e13e17 --- /dev/null +++ b/src/custom/DashboardLayout/DashboardLayout.tsx @@ -0,0 +1,134 @@ +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; + + /** Callback fired when the component requests to be closed (e.g. clicking the backdrop on mobile) */ + onClose?: () => void; +} + +export const DashboardLayout: React.FC = ({ + children, + isSidebarOpen, + sidebarContent, + sidebarWidth = { xs: '100%', md: '350px' }, + sidebarTopOffset = '0', + sidebarHeight = '100vh', + onClose, +}) => { + 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} + + + {isSidebarOpen && isMobile && ( + <> + setIsMobileDrawerOpen(false)} + onOpen={() => setIsMobileDrawerOpen(true)} + swipeAreaWidth={isMobileDrawerOpen ? drawerBleeding : 0} + disableSwipeToOpen={false} + ModalProps={{ + keepMounted: true, + }} + sx={{ + '& .MuiPaper-root': { + height: `calc(50% - ${drawerBleeding}px)`, + overflow: 'visible', + }, + '& .MuiDrawer-paper': { + borderTopLeftRadius: '16px', + borderTopRightRadius: '16px', + }, + }} + > + setIsMobileDrawerOpen(!isMobileDrawerOpen)} + > + + + + {sidebarContent} + + + + )} + + {isSidebarOpen && !isMobile && ( + + {sidebarContent} + + )} + + ); +}; diff --git a/src/custom/DashboardLayout/index.tsx b/src/custom/DashboardLayout/index.tsx index d5806b18a..552a08c05 100644 --- a/src/custom/DashboardLayout/index.tsx +++ b/src/custom/DashboardLayout/index.tsx @@ -1,137 +1,2 @@ -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} - - )} - - ); -}; +export { DashboardLayout } from './DashboardLayout'; +export type { DashboardLayoutProps } from './DashboardLayout'; diff --git a/src/custom/WidgetEmptyState/index.tsx b/src/custom/WidgetEmptyState/index.tsx new file mode 100644 index 000000000..c32613d66 --- /dev/null +++ b/src/custom/WidgetEmptyState/index.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { Box, Typography, Button, Stack } from '../../base'; +import { useTheme } from '../../theme'; + +export interface WidgetEmptyStateProps { + /** The message to display when no data is available */ + message?: string; + + /** Optional icon to display above the message */ + icon?: React.ReactNode; + + /** Optional action button configuration */ + action?: { + label: string; + onClick: () => void; + }; +} + +export const WidgetEmptyState: React.FC = ({ + message = 'No data available', + icon, + action, +}) => { + const theme = useTheme(); + + return ( + + + {icon && ( + + {icon} + + )} + + {message} + + {action && ( + + )} + + + ); +}; diff --git a/src/custom/WidgetPicker/index.tsx b/src/custom/WidgetPicker/index.tsx index 075b1fead..c33ea9de1 100644 --- a/src/custom/WidgetPicker/index.tsx +++ b/src/custom/WidgetPicker/index.tsx @@ -1 +1 @@ -export * from './WidgetPicker'; +export { WidgetPicker, type WidgetPickerProps, type WidgetItem } from './WidgetPicker'; diff --git a/src/custom/index.ts b/src/custom/index.ts index 8de85c68e..bfc9cd185 100644 --- a/src/custom/index.ts +++ b/src/custom/index.ts @@ -7,3 +7,4 @@ export * from './Modal'; export * from './RJSFFormWrapper'; export * from './StyledAccordion'; export * from './WidgetPicker'; +export * from './WidgetEmptyState'; diff --git a/src/index.tsx b/src/index.tsx index 2322b3375..83e19418f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -95,3 +95,8 @@ export { type WidgetPickerProps, type WidgetItem } from './custom/WidgetPicker'; + +export { + WidgetEmptyState, + type WidgetEmptyStateProps +} from './custom/WidgetEmptyState'; From 80ee06be4263ac4afec6a90ecd521230711eeeab Mon Sep 17 00:00:00 2001 From: NSTKrishna Date: Tue, 28 Jul 2026 02:24:36 +0530 Subject: [PATCH 07/10] Polish dashboard widgets and layout behavior This updates several dashboard-related components for better UX and consistency: DashboardLayout drops an unused `onClose` prop, adds keyboard-accessible sidebar toggle behavior, and uses theme divider color for the drawer handle. PlainCard and ResponsiveDataTable now show `WidgetEmptyState` when content is empty (including table `noMatch` text), and WidgetPicker now merges `containerSx` safely by supporting array/object forms. It also exports `DashboardLayout` from the custom barrel. Signed-off-by: NSTKrishna --- .../DashboardLayout/DashboardLayout.tsx | 17 +++++--- src/custom/DashboardWidgets/PlainCard.tsx | 43 +++++++++++-------- src/custom/ResponsiveDataTable.tsx | 16 +++++++ src/custom/WidgetPicker/WidgetPicker.tsx | 18 ++++---- src/custom/index.tsx | 1 + 5 files changed, 63 insertions(+), 32 deletions(-) diff --git a/src/custom/DashboardLayout/DashboardLayout.tsx b/src/custom/DashboardLayout/DashboardLayout.tsx index 721e13e17..3b71964d7 100644 --- a/src/custom/DashboardLayout/DashboardLayout.tsx +++ b/src/custom/DashboardLayout/DashboardLayout.tsx @@ -22,8 +22,6 @@ export interface DashboardLayoutProps { /** Optional fixed height for the sticky sidebar. Defaults to 100vh */ sidebarHeight?: string | number; - /** Callback fired when the component requests to be closed (e.g. clicking the backdrop on mobile) */ - onClose?: () => void; } export const DashboardLayout: React.FC = ({ @@ -32,8 +30,7 @@ export const DashboardLayout: React.FC = ({ sidebarContent, sidebarWidth = { xs: '100%', md: '350px' }, sidebarTopOffset = '0', - sidebarHeight = '100vh', - onClose, + sidebarHeight = '100vh' }) => { const theme = useTheme(); // We use the 'md' breakpoint (900px default) to switch between mobile and desktop layout @@ -80,6 +77,16 @@ export const DashboardLayout: React.FC = ({ }} > { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setIsMobileDrawerOpen(!isMobileDrawerOpen); + } + }} sx={{ position: 'absolute', top: -drawerBleeding, @@ -102,7 +109,7 @@ export const DashboardLayout: React.FC = ({ sx={{ width: 30, height: 6, - backgroundColor: theme.palette.mode === 'light' ? '#e0e0e0' : '#424242', + backgroundColor: theme.palette.divider, borderRadius: 3, }} /> diff --git a/src/custom/DashboardWidgets/PlainCard.tsx b/src/custom/DashboardWidgets/PlainCard.tsx index 9eeddd8c1..8eb2b38a4 100644 --- a/src/custom/DashboardWidgets/PlainCard.tsx +++ b/src/custom/DashboardWidgets/PlainCard.tsx @@ -1,6 +1,7 @@ import { Box, Card, CardContent, Link, Typography } from '../../base'; import { OpenInNewIcon } from '../../icons'; import { styled } from '../../theme'; +import { WidgetEmptyState } from '../WidgetEmptyState'; const StyledCard = styled(Card)(({ theme }) => ({ height: '100%', @@ -72,27 +73,31 @@ export const PlainCard = ({ title, icon, resources }: PlainCardProps): JSX.Eleme - - {resources.map((item) => ( - - {item.icon} + {resources.length === 0 ? ( + + ) : ( + + {resources.map((item) => ( + + {item.icon} - - {item.name} - + + {item.name} + - {item.external && ( - - - - )} - - ))} - + {item.external && ( + + + + )} + + ))} + + )} diff --git a/src/custom/ResponsiveDataTable.tsx b/src/custom/ResponsiveDataTable.tsx index 9678cf7d4..50cdb268d 100644 --- a/src/custom/ResponsiveDataTable.tsx +++ b/src/custom/ResponsiveDataTable.tsx @@ -8,6 +8,7 @@ import { styled, useTheme } from './../theme'; import { ColView } from './Helpers/ResponsiveColumns/responsive-coulmns.tsx'; import { TableAction } from './TableActions'; import { TooltipIcon } from './TooltipIconButton'; +import { WidgetEmptyState } from './WidgetEmptyState'; export const IconWrapper = styled('div', { shouldForwardProp: (prop) => prop !== 'disabled' @@ -158,8 +159,23 @@ const ResponsiveDataTable = ({ rowsPerPageOptions = [10, 25, 50, 100], ...props }: ResponsiveDataTableProps): JSX.Element => { + // Intercept the noMatch string to render a standardized empty state + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const textLabels: any = (options as any)?.textLabels || {}; + const bodyTextLabels = textLabels.body || {}; + + if (typeof bodyTextLabels.noMatch === 'string') { + bodyTextLabels.noMatch = ; + } + const updatedOptions = { ...options, + textLabels: { + ...textLabels, + body: { + ...bodyTextLabels + } + }, print: false, download: false, search: false, diff --git a/src/custom/WidgetPicker/WidgetPicker.tsx b/src/custom/WidgetPicker/WidgetPicker.tsx index 69483f37e..f6387f2ee 100644 --- a/src/custom/WidgetPicker/WidgetPicker.tsx +++ b/src/custom/WidgetPicker/WidgetPicker.tsx @@ -43,14 +43,16 @@ export const WidgetPicker: React.FC = ({ return ( Date: Tue, 28 Jul 2026 03:06:35 +0530 Subject: [PATCH 08/10] Type table options and normalize empty state Use `MUIDataTableOptions` for `ResponsiveDataTableProps.options` instead of `object`, removing `any` casts around `textLabels`. The no-match handling now consistently renders `WidgetEmptyState`, using the provided `body.noMatch` string when available and falling back to `"No data available"`. Signed-off-by: NSTKrishna --- src/custom/ResponsiveDataTable.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/custom/ResponsiveDataTable.tsx b/src/custom/ResponsiveDataTable.tsx index 50cdb268d..54eb93311 100644 --- a/src/custom/ResponsiveDataTable.tsx +++ b/src/custom/ResponsiveDataTable.tsx @@ -1,4 +1,4 @@ -import MUIDataTable, { MUIDataTableColumn } from '@sistent/mui-datatables'; +import MUIDataTable, { MUIDataTableColumn, MUIDataTableOptions } from '@sistent/mui-datatables'; import React, { useCallback } from 'react'; import { Checkbox, Collapse, ListItemIcon, ListItemText, Menu, MenuItem } from '../base'; import { ShareIcon } from '../icons'; @@ -142,7 +142,7 @@ export interface Column { export interface ResponsiveDataTableProps { data: string[][]; columns: MUIDataTableColumn[]; - options?: object; + options?: MUIDataTableOptions; tableCols?: MUIDataTableColumn[]; updateCols?: ((columns: MUIDataTableColumn[]) => void) | undefined; columnVisibility: Record | undefined; @@ -159,21 +159,21 @@ const ResponsiveDataTable = ({ rowsPerPageOptions = [10, 25, 50, 100], ...props }: ResponsiveDataTableProps): JSX.Element => { - // Intercept the noMatch string to render a standardized empty state - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const textLabels: any = (options as any)?.textLabels || {}; + const textLabels = options?.textLabels || {}; const bodyTextLabels = textLabels.body || {}; - if (typeof bodyTextLabels.noMatch === 'string') { - bodyTextLabels.noMatch = ; - } + const noMatchMessage = + typeof bodyTextLabels.noMatch === 'string' + ? bodyTextLabels.noMatch + : 'No data available'; const updatedOptions = { ...options, textLabels: { ...textLabels, body: { - ...bodyTextLabels + ...bodyTextLabels, + noMatch: } }, print: false, From 67f627f48d454afbf907649a5bd06092025680a3 Mon Sep 17 00:00:00 2001 From: NSTKrishna Date: Mon, 10 Aug 2026 21:59:43 +0530 Subject: [PATCH 09/10] fix(dashboard-layout): fix imports, state model, and structure issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Import Fab from '../../base' instead of '@mui/material' (optional-peer guard) - Import SxProps/Theme as 'import type' in WidgetPicker (erased at runtime) - Replace useEffect state mirror with useRef-guarded two-dimension model: isSheetVisible is owned by DashboardLayout and resets to true only when isSidebarOpen transitions OFF→ON, allowing users to minimize the sheet while Edit Mode stays active (FAB appears as the re-open affordance) - Move WidgetEmptyState body from index.tsx to WidgetEmptyState.tsx; index.tsx becomes a clean re-export barrel (consistent with other components) - Remove duplicate WidgetPicker/DashboardLayout exports from index.tsx (already covered by index.ts) Signed-off-by: NSTKrishna --- .../DashboardLayout/DashboardLayout.tsx | 117 +++++++----------- .../WidgetEmptyState/WidgetEmptyState.tsx | 81 ++++++++++++ src/custom/WidgetEmptyState/index.tsx | 83 +------------ src/custom/WidgetPicker/WidgetPicker.tsx | 2 +- src/custom/index.tsx | 2 - 5 files changed, 129 insertions(+), 156 deletions(-) create mode 100644 src/custom/WidgetEmptyState/WidgetEmptyState.tsx diff --git a/src/custom/DashboardLayout/DashboardLayout.tsx b/src/custom/DashboardLayout/DashboardLayout.tsx index 3b71964d7..91887e7b6 100644 --- a/src/custom/DashboardLayout/DashboardLayout.tsx +++ b/src/custom/DashboardLayout/DashboardLayout.tsx @@ -1,13 +1,15 @@ -import React, { useState, useEffect } from 'react'; -import { Box } from '../../base'; +import React, { useState, useEffect, useRef } from 'react'; +import { Box, Fab } from '../../base'; +import { AddIcon } from '../../icons/Add'; import { useTheme, useMediaQuery } from '../../theme'; -import { SwipeableDrawer } from '@mui/material'; +import { BottomSheet } from '../BottomSheet'; export interface DashboardLayoutProps { /** The main dashboard content (typically the React-Grid-Layout) */ children: React.ReactNode; - /** Whether the right-hand sidebar should be visible */ + /** Whether Edit Mode is active (controls sidebar visibility). When this + * transitions from false → true the mobile sheet auto-opens. */ isSidebarOpen: boolean; /** The content to render inside the sidebar (e.g., Widget Gallery) */ @@ -21,7 +23,6 @@ export interface DashboardLayoutProps { /** Optional fixed height for the sticky sidebar. Defaults to 100vh */ sidebarHeight?: string | number; - } export const DashboardLayout: React.FC = ({ @@ -36,15 +37,25 @@ export const DashboardLayout: React.FC = ({ // 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; + // isSheetVisible is independently owned by DashboardLayout: + // - resets to true whenever Edit Mode (isSidebarOpen) transitions OFF → ON + // - can be set to false by the user dismissing the sheet (FAB appears instead) + // - set to false when Edit Mode turns OFF + // This two-dimension model prevents the sheet from re-opening on every + // isSidebarOpen change after the user has intentionally minimized it. + const [isSheetVisible, setIsSheetVisible] = useState(isSidebarOpen); + const prevIsSidebarOpen = useRef(isSidebarOpen); useEffect(() => { - if (isSidebarOpen) { - setIsMobileDrawerOpen(true); - } else { - setIsMobileDrawerOpen(false); + if (isSidebarOpen && !prevIsSidebarOpen.current) { + // Edit Mode just turned ON → pop the sheet open + setIsSheetVisible(true); + } + if (!isSidebarOpen) { + // Edit Mode turned OFF → close the sheet and hide the FAB + setIsSheetVisible(false); } + prevIsSidebarOpen.current = isSidebarOpen; }, [isSidebarOpen]); return ( @@ -55,69 +66,31 @@ export const DashboardLayout: React.FC = ({ {isSidebarOpen && isMobile && ( <> - setIsMobileDrawerOpen(false)} - onOpen={() => setIsMobileDrawerOpen(true)} - swipeAreaWidth={isMobileDrawerOpen ? drawerBleeding : 0} - disableSwipeToOpen={false} - ModalProps={{ - keepMounted: true, - }} - sx={{ - '& .MuiPaper-root': { - height: `calc(50% - ${drawerBleeding}px)`, - overflow: 'visible', - }, - '& .MuiDrawer-paper': { - borderTopLeftRadius: '16px', - borderTopRightRadius: '16px', - }, - }} + setIsSheetVisible(false)} + maxHeight="50vh" > - { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - setIsMobileDrawerOpen(!isMobileDrawerOpen); - } - }} - sx={{ - position: 'absolute', - top: -drawerBleeding, - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - visibility: 'visible', - right: 0, - left: 0, - backgroundColor: theme.palette.background.paper, - height: drawerBleeding, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - borderBottom: `1px solid ${theme.palette.divider}`, - cursor: 'pointer', - }} - onClick={() => setIsMobileDrawerOpen(!isMobileDrawerOpen)} + {sidebarContent} + + + {/* FAB appears when Edit Mode is active but the sheet has been minimized, + letting users rearrange the dashboard and pull the picker back up. */} + {!isSheetVisible && ( + setIsSheetVisible(true)} + sx={(fabTheme) => ({ + position: 'fixed', + bottom: 24, + right: 24, + zIndex: fabTheme.zIndex.drawer, + })} > - - - - {sidebarContent} - - + + + )} )} diff --git a/src/custom/WidgetEmptyState/WidgetEmptyState.tsx b/src/custom/WidgetEmptyState/WidgetEmptyState.tsx new file mode 100644 index 000000000..c32613d66 --- /dev/null +++ b/src/custom/WidgetEmptyState/WidgetEmptyState.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { Box, Typography, Button, Stack } from '../../base'; +import { useTheme } from '../../theme'; + +export interface WidgetEmptyStateProps { + /** The message to display when no data is available */ + message?: string; + + /** Optional icon to display above the message */ + icon?: React.ReactNode; + + /** Optional action button configuration */ + action?: { + label: string; + onClick: () => void; + }; +} + +export const WidgetEmptyState: React.FC = ({ + message = 'No data available', + icon, + action, +}) => { + const theme = useTheme(); + + return ( + + + {icon && ( + + {icon} + + )} + + {message} + + {action && ( + + )} + + + ); +}; diff --git a/src/custom/WidgetEmptyState/index.tsx b/src/custom/WidgetEmptyState/index.tsx index c32613d66..334bd8b95 100644 --- a/src/custom/WidgetEmptyState/index.tsx +++ b/src/custom/WidgetEmptyState/index.tsx @@ -1,81 +1,2 @@ -import React from 'react'; -import { Box, Typography, Button, Stack } from '../../base'; -import { useTheme } from '../../theme'; - -export interface WidgetEmptyStateProps { - /** The message to display when no data is available */ - message?: string; - - /** Optional icon to display above the message */ - icon?: React.ReactNode; - - /** Optional action button configuration */ - action?: { - label: string; - onClick: () => void; - }; -} - -export const WidgetEmptyState: React.FC = ({ - message = 'No data available', - icon, - action, -}) => { - const theme = useTheme(); - - return ( - - - {icon && ( - - {icon} - - )} - - {message} - - {action && ( - - )} - - - ); -}; +export { WidgetEmptyState } from './WidgetEmptyState'; +export type { WidgetEmptyStateProps } from './WidgetEmptyState'; diff --git a/src/custom/WidgetPicker/WidgetPicker.tsx b/src/custom/WidgetPicker/WidgetPicker.tsx index f6387f2ee..15aca9415 100644 --- a/src/custom/WidgetPicker/WidgetPicker.tsx +++ b/src/custom/WidgetPicker/WidgetPicker.tsx @@ -2,7 +2,7 @@ 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'; +import type { SxProps, Theme } from '@mui/material'; export interface WidgetItem { key: string; diff --git a/src/custom/index.tsx b/src/custom/index.tsx index b691ff357..bff31634c 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -193,5 +193,3 @@ export * from './RJSFFormWrapper'; export * from './ShareModal'; export * from './UserSearchField'; export * from './Workspaces'; -export * from './WidgetPicker'; -export * from './DashboardLayout'; From de805c2ef0efc6d2ccc702261984c80d981a5ec8 Mon Sep 17 00:00:00 2001 From: NSTKrishna Date: Mon, 10 Aug 2026 22:14:40 +0530 Subject: [PATCH 10/10] fix(a11y): address CodeRabbit accessibility feedback - DashboardLayout: add sidebarTitle prop (default 'Widget Picker') and pass it to BottomSheet so the Dialog gets an aria-labelledby label; without a title the dialog is unlabelled for screen readers - WidgetEmptyState: narrow role=status + aria-live=polite to the message Typography only; the outer Box and Button siblings are removed from the live region to prevent AT from hiding or degrading interactive semantics Signed-off-by: NSTKrishna --- src/custom/DashboardLayout/DashboardLayout.tsx | 6 ++++++ src/custom/WidgetEmptyState/WidgetEmptyState.tsx | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/custom/DashboardLayout/DashboardLayout.tsx b/src/custom/DashboardLayout/DashboardLayout.tsx index 91887e7b6..acf80415f 100644 --- a/src/custom/DashboardLayout/DashboardLayout.tsx +++ b/src/custom/DashboardLayout/DashboardLayout.tsx @@ -15,6 +15,10 @@ export interface DashboardLayoutProps { /** The content to render inside the sidebar (e.g., Widget Gallery) */ sidebarContent: React.ReactNode; + /** Accessible title for the mobile bottom sheet (used as aria-labelledby on the Dialog). + * Defaults to 'Widget Picker'. */ + sidebarTitle?: string; + /** Optional custom width for the sidebar. Defaults to responsive width. */ sidebarWidth?: string | number | Partial>; @@ -29,6 +33,7 @@ export const DashboardLayout: React.FC = ({ children, isSidebarOpen, sidebarContent, + sidebarTitle = 'Widget Picker', sidebarWidth = { xs: '100%', md: '350px' }, sidebarTopOffset = '0', sidebarHeight = '100vh' @@ -69,6 +74,7 @@ export const DashboardLayout: React.FC = ({ setIsSheetVisible(false)} + title={sidebarTitle} maxHeight="50vh" > {sidebarContent} diff --git a/src/custom/WidgetEmptyState/WidgetEmptyState.tsx b/src/custom/WidgetEmptyState/WidgetEmptyState.tsx index c32613d66..a2098ce61 100644 --- a/src/custom/WidgetEmptyState/WidgetEmptyState.tsx +++ b/src/custom/WidgetEmptyState/WidgetEmptyState.tsx @@ -24,9 +24,10 @@ export const WidgetEmptyState: React.FC = ({ const theme = useTheme(); return ( + // Outer container is a plain presentational box — role="status" is scoped + // only to the message Typography below so interactive descendants (icon, Button) + // are not degraded by the live-region semantics. = ({ {icon} )} + {/* role="status" + aria-live scoped only to the message text, not the + interactive siblings — per ARIA spec, live regions must not contain + interactive elements or AT may hide/degrade their semantics. */}