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