This commit is contained in:
2025-09-01 17:17:27 -04:00
parent aa524d8ac7
commit 74b25eba27
33 changed files with 669 additions and 686 deletions

View File

@ -20,6 +20,9 @@ export interface SidebarProps {
footer?: React.ReactNode;
children: React.ReactNode;
// Layout mode - when true, sidebar fills container instead of fixed positioning
layoutMode?: boolean;
// Styling customization
zIndex?: number;
offsetTop?: number;
@ -42,6 +45,7 @@ const Sidebar: React.FC<SidebarProps> = ({
headerActions,
footer,
children,
layoutMode = false,
zIndex = 1000,
offsetTop = 60,
backgroundColor = 'var(--mantine-color-body)',
@ -49,30 +53,46 @@ const Sidebar: React.FC<SidebarProps> = ({
animationDuration = '0.3s',
'aria-label': ariaLabel,
}) => {
// Calculate position styles based on position prop
// Calculate position styles based on layout mode
const getPositionStyles = () => {
const baseStyles = {
position: 'fixed' as const,
top: offsetTop,
bottom: 0,
width: `${width}px`,
zIndex,
borderRadius: 0,
display: 'flex',
flexDirection: 'column' as const,
backgroundColor,
height: '100%',
};
if (layoutMode) {
// In layout mode, sidebar fills its container (managed by SidebarLayout)
return {
...baseStyles,
position: 'relative' as const,
borderLeft: position === 'right' ? `1px solid ${borderColor}` : undefined,
borderRight: position === 'left' ? `1px solid ${borderColor}` : undefined,
};
}
// Legacy fixed positioning mode (for backward compatibility)
const fixedStyles = {
...baseStyles,
position: 'fixed' as const,
top: offsetTop,
bottom: 0,
zIndex,
transition: `${position} ${animationDuration} ease`,
};
if (position === 'right') {
return {
...baseStyles,
...fixedStyles,
right: opened ? 0 : `-${width}px`,
borderLeft: `1px solid ${borderColor}`,
};
} else {
return {
...baseStyles,
...fixedStyles,
left: opened ? 0 : `-${width}px`,
borderRight: `1px solid ${borderColor}`,
};

View File

@ -0,0 +1,170 @@
import React from 'react';
import { Box } from '@mantine/core';
export interface SidebarLayoutProps {
children: React.ReactNode;
sidebar?: React.ReactNode;
sidebarOpened?: boolean;
sidebarWidth?: number;
sidebarPosition?: 'left' | 'right';
offsetTop?: number;
className?: string;
// Animation settings
transitionDuration?: string;
}
/**
* SidebarLayout provides a responsive layout that shrinks the main content area
* when a sidebar is opened, rather than overlaying on top of the content.
*
* This ensures the main content remains visible and accessible when sidebars are open.
*/
const SidebarLayout: React.FC<SidebarLayoutProps> = ({
children,
sidebar,
sidebarOpened = false,
sidebarWidth = 450,
sidebarPosition = 'right',
offsetTop = 60,
transitionDuration = '0.3s',
className,
}) => {
// Calculate main content area margins based on sidebar state
const getMainContentStyles = (): React.CSSProperties => {
if (!sidebarOpened) {
return {
marginLeft: 0,
marginRight: 0,
transition: `margin ${transitionDuration} ease`,
};
}
return {
marginLeft: sidebarPosition === 'left' ? `${sidebarWidth}px` : 0,
marginRight: sidebarPosition === 'right' ? `${sidebarWidth}px` : 0,
transition: `margin ${transitionDuration} ease`,
};
};
// Calculate sidebar container styles for proper positioning
const getSidebarContainerStyles = (): React.CSSProperties => ({
position: 'fixed',
top: offsetTop,
bottom: 0,
width: `${sidebarWidth}px`,
zIndex: 1000,
[sidebarPosition]: sidebarOpened ? 0 : `-${sidebarWidth}px`,
transition: `${sidebarPosition} ${transitionDuration} ease`,
pointerEvents: sidebarOpened ? 'auto' : 'none',
});
return (
<Box className={className} style={{ position: 'relative', minHeight: '100%' }}>
{/* Main Content Area - adjusts width based on sidebar state */}
<Box style={getMainContentStyles()}>
{children}
</Box>
{/* Sidebar Container - positioned absolutely but doesn't overlay content */}
{sidebar && (
<Box style={getSidebarContainerStyles()}>
{sidebar}
</Box>
)}
</Box>
);
};
export default SidebarLayout;
/**
* Higher-level wrapper that combines SidebarLayout with responsive behavior
* and mobile-friendly overlays when screen size is too small.
*/
export interface ResponsiveSidebarLayoutProps extends SidebarLayoutProps {
mobileBreakpoint?: number;
overlayOnMobile?: boolean;
}
export const ResponsiveSidebarLayout: React.FC<ResponsiveSidebarLayoutProps> = ({
mobileBreakpoint = 768,
overlayOnMobile = true,
...props
}) => {
const [isMobile, setIsMobile] = React.useState(false);
React.useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < mobileBreakpoint);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, [mobileBreakpoint]);
// On mobile, use overlay behavior instead of shrinking content
if (isMobile && overlayOnMobile) {
return (
<Box style={{ position: 'relative', minHeight: '100%' }}>
{props.children}
{props.sidebar && props.sidebarOpened && (
<Box
style={{
position: 'fixed',
top: props.offsetTop || 60,
bottom: 0,
[props.sidebarPosition || 'right']: 0,
width: `${props.sidebarWidth || 450}px`,
zIndex: 1000,
transition: `transform ${props.transitionDuration || '0.3s'} ease`,
}}
>
{props.sidebar}
</Box>
)}
{/* Mobile overlay backdrop */}
{props.sidebarOpened && (
<Box
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 999,
}}
onClick={() => {
// If sidebar has onClose, call it
if (React.isValidElement(props.sidebar) && props.sidebar.props.onClose) {
props.sidebar.props.onClose();
}
}}
/>
)}
</Box>
);
}
return <SidebarLayout {...props} />;
};
// Hook for managing sidebar layout state
export const useSidebarLayout = (initialOpened = false) => {
const [sidebarOpened, setSidebarOpened] = React.useState(initialOpened);
const openSidebar = React.useCallback(() => setSidebarOpened(true), []);
const closeSidebar = React.useCallback(() => setSidebarOpened(false), []);
const toggleSidebar = React.useCallback(() => setSidebarOpened(prev => !prev), []);
return {
sidebarOpened,
openSidebar,
closeSidebar,
toggleSidebar,
setSidebarOpened,
};
};

View File

@ -4,6 +4,7 @@ export { default as DataTable } from './components/DataTable/DataTable';
export { default as StatusBadge, UserRoleBadge, ApplicationTypeBadge, RuntimeBadge, ExecutionStatusBadge, SeverityBadge } from './components/StatusBadge/StatusBadge';
export { default as EmptyState, NoUsersState, NoApplicationsState, NoFunctionsState, NoTokensState, NoSearchResults, ErrorState } from './components/EmptyState/EmptyState';
export { default as Sidebar, FormSidebarWrapper, DetailsSidebar, QuickSidebar, useSidebar } from './components/Sidebar/Sidebar';
export { default as SidebarLayout, ResponsiveSidebarLayout, useSidebarLayout } from './components/SidebarLayout/SidebarLayout';
export { default as ActionMenu, createViewAction, createEditAction, createCopyAction, createDeleteAction, createArchiveAction, createRestoreAction, getUserActions, getApplicationActions, getFunctionActions, getTokenActions } from './components/ActionMenu/ActionMenu';
export { default as LoadingState, TableLoadingState, CardsLoadingState, FormLoadingState, PageLoadingState, InlineLoadingState, useLoadingState } from './components/LoadingState/LoadingState';