Refactor Dashboard Layout and Sidebar Components for Improved Responsiveness
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced a new DashboardSidebarSplitter component to manage sidebar resizing and collapsing behavior, enhancing user experience on different screen sizes. - Updated DashboardSidebar to support dynamic collapsed width and improved styling for better visual consistency. - Refactored DashboardLayout to utilize the new splitter, streamlining the layout structure and improving maintainability. - Enhanced CSS styles for sidebar components to ensure consistent behavior and appearance across various states.
This commit is contained in:
parent
4ea6d1e7a3
commit
5e46de8b60
@ -253,8 +253,8 @@ html.macos-vibrancy .dark-mode .ant-layout.main-content-layout .ant-card {
|
||||
}
|
||||
|
||||
.electron-sider.ant-layout-sider-collapsed {
|
||||
max-width: 55px !important;
|
||||
min-width: 55px !important;
|
||||
min-width: var(--dashboard-sidebar-collapsed-width, 55px) !important;
|
||||
max-width: var(--dashboard-sidebar-collapsed-width, 55px) !important;
|
||||
}
|
||||
|
||||
.loading-modal .ant-modal-footer {
|
||||
@ -362,8 +362,10 @@ code {
|
||||
}
|
||||
|
||||
.cursor-tooltip .ant-card {
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
box-shadow:
|
||||
0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* --- Start of src/index.css --- */
|
||||
@ -412,9 +414,21 @@ body {
|
||||
|
||||
/* --- Start of src/components/Dashboard/common/DashboardSidebar.css --- */
|
||||
.ant-layout-sider {
|
||||
width: 250px; /* Adjust the width as per your preference */
|
||||
min-width: 250px; /* Ensure a minimum width to avoid collapsing */
|
||||
max-width: 400px; /* Optionally set a maximum width */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dashboard-sider.ant-layout-sider {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
max-width: 100% !important;
|
||||
flex: 0 0 100% !important;
|
||||
}
|
||||
|
||||
.dashboard-sider.ant-layout-sider-collapsed {
|
||||
width: var(--dashboard-sidebar-collapsed-width, 80px) !important;
|
||||
min-width: var(--dashboard-sidebar-collapsed-width, 80px) !important;
|
||||
max-width: var(--dashboard-sidebar-collapsed-width, 80px) !important;
|
||||
flex: 0 0 var(--dashboard-sidebar-collapsed-width, 80px) !important;
|
||||
}
|
||||
/* --- End of src/components/Dashboard/common/DashboardSidebar.css --- */
|
||||
|
||||
@ -995,6 +1009,11 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.farmcontrol-splitter.ant-splitter-horizontal.is-size-transitioning
|
||||
.ant-splitter-panel {
|
||||
transition: flex-basis 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.farmcontrol-splitter
|
||||
.ant-splitter-panel:has(~ .ant-splitter-panel)
|
||||
.info-collapse.ant-collapse.ant-collapse-icon-position-end
|
||||
@ -1355,7 +1374,8 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
||||
1,
|
||||
abs(
|
||||
calc(
|
||||
var(--permissions-col, 0) - var(--permissions-hovered-col, -1)
|
||||
var(--permissions-col, 0) -
|
||||
var(--permissions-hovered-col, -1)
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -1512,3 +1532,19 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
||||
stroke: currentColor;
|
||||
stroke-width: 8px;
|
||||
}
|
||||
|
||||
.dashboard-splitter.farmcontrol-splitter > .ant-splitter-bar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dashboard-splitter.farmcontrol-splitter
|
||||
> .ant-splitter-bar
|
||||
.ant-splitter-bar-dragger::before {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dashboard-splitter.farmcontrol-splitter
|
||||
> .ant-splitter-bar
|
||||
.ant-splitter-bar-dragger::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import ManagementSidebar from './Management/ManagementSidebar'
|
||||
import DashboardNavigation from './common/DashboardNavigation'
|
||||
import DashboardBreadcrumb from './common/DashboardBreadcrumb'
|
||||
import DeveloperSidebar from './Developer/DeveloperSidebar'
|
||||
import DashboardSidebarSplitter from './common/DashboardSidebarSplitter'
|
||||
import { useThemeContext } from './context/ThemeContext'
|
||||
import { MessageProvider } from './context/MessageContext'
|
||||
|
||||
@ -26,6 +27,22 @@ const DashboardLayout = ({ children }) => {
|
||||
|
||||
const { isDarkMode } = useThemeContext()
|
||||
|
||||
const sidebar = isProduction ? (
|
||||
<ProductionSidebar />
|
||||
) : isInventory ? (
|
||||
<InventorySidebar />
|
||||
) : isFinance ? (
|
||||
<FinanceSidebar />
|
||||
) : isSales ? (
|
||||
<SalesSidebar />
|
||||
) : isManagement ? (
|
||||
<ManagementSidebar />
|
||||
) : isDeveloper ? (
|
||||
<DeveloperSidebar />
|
||||
) : (
|
||||
<ProductionSidebar />
|
||||
)
|
||||
|
||||
return (
|
||||
<MessageProvider>
|
||||
<Layout
|
||||
@ -33,34 +50,18 @@ const DashboardLayout = ({ children }) => {
|
||||
className={`${isDarkMode ? 'dark-mode' : 'light-mode'} main-layout`}
|
||||
>
|
||||
<DashboardNavigation />
|
||||
<Layout>
|
||||
{isProduction ? (
|
||||
<ProductionSidebar />
|
||||
) : isInventory ? (
|
||||
<InventorySidebar />
|
||||
) : isFinance ? (
|
||||
<FinanceSidebar />
|
||||
) : isSales ? (
|
||||
<SalesSidebar />
|
||||
) : isManagement ? (
|
||||
<ManagementSidebar />
|
||||
) : isDeveloper ? (
|
||||
<DeveloperSidebar />
|
||||
) : (
|
||||
<ProductionSidebar /> // Default to production sidebar
|
||||
)}
|
||||
<DashboardSidebarSplitter sidebar={sidebar}>
|
||||
<Layout style={{ padding: '24px' }} className='main-content-layout'>
|
||||
<Content>
|
||||
<Flex vertical style={{ height: '100%' }} gap='20px'>
|
||||
<Flex justify='space-between'>
|
||||
<DashboardBreadcrumb style={{ margin: '16px 0' }} />
|
||||
</Flex>
|
||||
|
||||
{children}
|
||||
</Flex>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</DashboardSidebarSplitter>
|
||||
</Layout>
|
||||
</MessageProvider>
|
||||
)
|
||||
|
||||
@ -40,6 +40,7 @@ const DashboardSidebar = ({
|
||||
selectedKey = '',
|
||||
onCollapse,
|
||||
collapsed: collapsedProp,
|
||||
collapsedWidth: collapsedWidthProp,
|
||||
key = 'DashboardSidebar_collapseState'
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = useState(() => {
|
||||
@ -51,6 +52,7 @@ const DashboardSidebar = ({
|
||||
const navigate = useNavigate()
|
||||
const { isDarkMode } = useThemeContext()
|
||||
const { isElectron } = useContext(ElectronContext)
|
||||
const collapsedWidth = collapsedWidthProp ?? (isElectron ? 55 : 80)
|
||||
const { userProfile } = useContext(AuthContext)
|
||||
const allowedItems = filterSidebarItemsByListPermission(items, userProfile)
|
||||
|
||||
@ -117,10 +119,14 @@ const DashboardSidebar = ({
|
||||
|
||||
return (
|
||||
<Sider
|
||||
width={250}
|
||||
width='100%'
|
||||
collapsedWidth={collapsedWidth}
|
||||
theme='light'
|
||||
collapsed={collapsed}
|
||||
className={isElectron ? 'electron-sider' : ''}
|
||||
className={`dashboard-sider${isElectron ? ' electron-sider' : ''}`}
|
||||
style={{
|
||||
'--dashboard-sidebar-collapsed-width': `${collapsedWidth}px`
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
style={{ height: '100%', minHeight: 0 }}
|
||||
@ -159,6 +165,7 @@ DashboardSidebar.propTypes = {
|
||||
selectedKey: PropTypes.string,
|
||||
onCollapse: PropTypes.func,
|
||||
collapsed: PropTypes.bool,
|
||||
collapsedWidth: PropTypes.number,
|
||||
key: PropTypes.string
|
||||
}
|
||||
|
||||
|
||||
222
src/components/Dashboard/common/DashboardSidebarSplitter.jsx
Normal file
222
src/components/Dashboard/common/DashboardSidebarSplitter.jsx
Normal file
@ -0,0 +1,222 @@
|
||||
import {
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Layout, Splitter } from 'antd'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { ElectronContext } from '../context/ElectronContext'
|
||||
|
||||
const COLLAPSE_BELOW_PX = 150
|
||||
const DEFAULT_SIDEBAR_PX = 300
|
||||
const MAX_SIDEBAR_PX = 400
|
||||
const COLLAPSED_WIDTH_PX = 80
|
||||
const ELECTRON_COLLAPSED_WIDTH_PX = 55
|
||||
const COLLAPSE_STORAGE_KEY = 'DashboardSidebar_collapseState'
|
||||
const SIZE_STORAGE_KEY = 'DashboardSidebar_width'
|
||||
|
||||
const readStoredBoolean = (key, fallback) => {
|
||||
try {
|
||||
const saved = sessionStorage.getItem(key)
|
||||
return saved ? JSON.parse(saved) : fallback
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
const readStoredNumber = (key, fallback) => {
|
||||
try {
|
||||
const saved = sessionStorage.getItem(key)
|
||||
const value = saved ? JSON.parse(saved) : fallback
|
||||
return typeof value === 'number' && !Number.isNaN(value) ? value : fallback
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
const clampExpandedSize = (size) =>
|
||||
Math.min(MAX_SIDEBAR_PX, Math.max(size, COLLAPSE_BELOW_PX))
|
||||
|
||||
const DashboardSidebarSplitter = ({ sidebar, children }) => {
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const { isElectron } = useContext(ElectronContext)
|
||||
const collapsedWidth = isElectron
|
||||
? ELECTRON_COLLAPSED_WIDTH_PX
|
||||
: COLLAPSED_WIDTH_PX
|
||||
|
||||
const [collapsed, setCollapsed] = useState(() =>
|
||||
readStoredBoolean(COLLAPSE_STORAGE_KEY, false)
|
||||
)
|
||||
const [sidebarSize, setSidebarSize] = useState(() => {
|
||||
if (readStoredBoolean(COLLAPSE_STORAGE_KEY, false)) {
|
||||
return isElectron ? ELECTRON_COLLAPSED_WIDTH_PX : COLLAPSED_WIDTH_PX
|
||||
}
|
||||
return clampExpandedSize(
|
||||
readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX)
|
||||
)
|
||||
})
|
||||
|
||||
const collapsedRef = useRef(collapsed)
|
||||
collapsedRef.current = collapsed
|
||||
const lastExpandedSizeRef = useRef(
|
||||
clampExpandedSize(readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX))
|
||||
)
|
||||
const [sizeTransitioning, setSizeTransitioning] = useState(false)
|
||||
const sizeTransitionTimeoutRef = useRef(null)
|
||||
|
||||
const startSizeTransition = useCallback(() => {
|
||||
setSizeTransitioning(true)
|
||||
if (sizeTransitionTimeoutRef.current) {
|
||||
clearTimeout(sizeTransitionTimeoutRef.current)
|
||||
}
|
||||
sizeTransitionTimeoutRef.current = setTimeout(() => {
|
||||
setSizeTransitioning(false)
|
||||
sizeTransitionTimeoutRef.current = null
|
||||
}, 150)
|
||||
}, [])
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (sizeTransitionTimeoutRef.current) {
|
||||
clearTimeout(sizeTransitionTimeoutRef.current)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const persistCollapsed = (nextCollapsed) => {
|
||||
sessionStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(nextCollapsed))
|
||||
}
|
||||
|
||||
const persistSize = (size) => {
|
||||
sessionStorage.setItem(SIZE_STORAGE_KEY, JSON.stringify(size))
|
||||
}
|
||||
|
||||
const collapseSidebar = useCallback(
|
||||
(expandedSize) => {
|
||||
if (expandedSize >= COLLAPSE_BELOW_PX) {
|
||||
lastExpandedSizeRef.current = expandedSize
|
||||
persistSize(expandedSize)
|
||||
}
|
||||
collapsedRef.current = true
|
||||
setCollapsed(true)
|
||||
persistCollapsed(true)
|
||||
startSizeTransition()
|
||||
setSidebarSize(collapsedWidth)
|
||||
},
|
||||
[collapsedWidth, startSizeTransition]
|
||||
)
|
||||
|
||||
const expandSidebar = useCallback(() => {
|
||||
collapsedRef.current = false
|
||||
setCollapsed(false)
|
||||
persistCollapsed(false)
|
||||
startSizeTransition()
|
||||
setSidebarSize(DEFAULT_SIDEBAR_PX)
|
||||
persistSize(DEFAULT_SIDEBAR_PX)
|
||||
lastExpandedSizeRef.current = DEFAULT_SIDEBAR_PX
|
||||
}, [startSizeTransition])
|
||||
|
||||
const handleSidebarCollapse = useCallback(
|
||||
(nextCollapsed) => {
|
||||
if (nextCollapsed) {
|
||||
collapseSidebar(sidebarSize)
|
||||
} else {
|
||||
expandSidebar()
|
||||
}
|
||||
},
|
||||
[collapseSidebar, expandSidebar, sidebarSize]
|
||||
)
|
||||
|
||||
const handleResize = useCallback(
|
||||
(sizes) => {
|
||||
if (collapsedRef.current) {
|
||||
setSidebarSize(collapsedWidth)
|
||||
return
|
||||
}
|
||||
|
||||
const size = sizes[0]
|
||||
if (size < COLLAPSE_BELOW_PX) {
|
||||
collapseSidebar(lastExpandedSizeRef.current)
|
||||
return
|
||||
}
|
||||
|
||||
setSidebarSize(size)
|
||||
lastExpandedSizeRef.current = clampExpandedSize(size)
|
||||
},
|
||||
[collapseSidebar, collapsedWidth]
|
||||
)
|
||||
|
||||
const handleResizeEnd = useCallback(
|
||||
(sizes) => {
|
||||
if (collapsedRef.current) {
|
||||
setSidebarSize(collapsedWidth)
|
||||
return
|
||||
}
|
||||
|
||||
const next = clampExpandedSize(sizes[0])
|
||||
lastExpandedSizeRef.current = next
|
||||
setSidebarSize(next)
|
||||
persistSize(next)
|
||||
},
|
||||
[collapsedWidth]
|
||||
)
|
||||
|
||||
const sidebarNode = isValidElement(sidebar)
|
||||
? cloneElement(sidebar, {
|
||||
collapsed,
|
||||
collapsedWidth,
|
||||
onCollapse: handleSidebarCollapse
|
||||
})
|
||||
: sidebar
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Layout>
|
||||
{sidebarNode}
|
||||
{children}
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100%', minHeight: 0 }}>
|
||||
<Splitter
|
||||
className={`farmcontrol-splitter dashboard-splitter${
|
||||
sizeTransitioning ? ' is-size-transitioning' : ''
|
||||
}`}
|
||||
style={{ height: '100%' }}
|
||||
onResize={handleResize}
|
||||
onResizeEnd={handleResizeEnd}
|
||||
>
|
||||
<Splitter.Panel
|
||||
size={sidebarSize}
|
||||
min={collapsedWidth}
|
||||
max={collapsed ? collapsedWidth : MAX_SIDEBAR_PX}
|
||||
resizable={!collapsed}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
minWidth: collapsedWidth,
|
||||
maxWidth:
|
||||
collapsed && !sizeTransitioning ? collapsedWidth : undefined
|
||||
}}
|
||||
>
|
||||
{sidebarNode}
|
||||
</Splitter.Panel>
|
||||
<Splitter.Panel min={320}>{children}</Splitter.Panel>
|
||||
</Splitter>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
DashboardSidebarSplitter.propTypes = {
|
||||
sidebar: PropTypes.node.isRequired,
|
||||
children: PropTypes.node.isRequired
|
||||
}
|
||||
|
||||
export default DashboardSidebarSplitter
|
||||
Loading…
x
Reference in New Issue
Block a user