-
This commit is contained in:
@ -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}`,
|
||||
};
|
||||
|
||||
170
web-components/src/components/SidebarLayout/SidebarLayout.tsx
Normal file
170
web-components/src/components/SidebarLayout/SidebarLayout.tsx
Normal 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,
|
||||
};
|
||||
};
|
||||
@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user