diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index c49b782f..23b7ad3d 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -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; +} diff --git a/src/components/Dashboard/Layout.jsx b/src/components/Dashboard/Layout.jsx index 596a5640..0436c120 100644 --- a/src/components/Dashboard/Layout.jsx +++ b/src/components/Dashboard/Layout.jsx @@ -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 ? ( + + ) : isInventory ? ( + + ) : isFinance ? ( + + ) : isSales ? ( + + ) : isManagement ? ( + + ) : isDeveloper ? ( + + ) : ( + + ) + return ( { className={`${isDarkMode ? 'dark-mode' : 'light-mode'} main-layout`} > - - {isProduction ? ( - - ) : isInventory ? ( - - ) : isFinance ? ( - - ) : isSales ? ( - - ) : isManagement ? ( - - ) : isDeveloper ? ( - - ) : ( - // Default to production sidebar - )} + - {children} - + ) diff --git a/src/components/Dashboard/common/DashboardSidebar.jsx b/src/components/Dashboard/common/DashboardSidebar.jsx index 67181982..9438d5c9 100644 --- a/src/components/Dashboard/common/DashboardSidebar.jsx +++ b/src/components/Dashboard/common/DashboardSidebar.jsx @@ -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 ( { + 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 ( + + {sidebarNode} + {children} + + ) + } + + return ( + + + + {sidebarNode} + + {children} + + + ) +} + +DashboardSidebarSplitter.propTypes = { + sidebar: PropTypes.node.isRequired, + children: PropTypes.node.isRequired +} + +export default DashboardSidebarSplitter