first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
.dashboard-grid {
display: grid;
grid-template-areas: "navbar navbar navbar"
"sidenav-left header header"
"sidenav-left content sidenav-right"
"footer footer footer";
grid-template-rows: auto auto 1fr;
grid-template-columns: auto 1fr auto;
}
.dashboard-grid-content {
grid-area: content;
}
.dashboard-grid-header {
grid-area: header;
}
.dashboard-grid-navbar {
grid-area: navbar;
}
.dashboard-grid-sidenav-left {
grid-area: sidenav-left;
}
.dashboard-grid-sidenav-right{
grid-area: sidenav-right;
}
.dashboard-grid-footer {
grid-area: footer;
}

View File

@@ -0,0 +1,15 @@
import {ReactNode} from 'react';
import clsx from 'clsx';
interface DashboardContentHeaderProps {
children: ReactNode;
className?: string;
}
export function DashboardContentHeader({
children,
className,
}: DashboardContentHeaderProps) {
return (
<div className={clsx(className, 'dashboard-grid-header')}>{children}</div>
);
}

View File

@@ -0,0 +1,19 @@
import {cloneElement, ReactElement} from 'react';
import clsx from 'clsx';
interface DashboardContentProps {
children: ReactElement<{className: string}>;
isScrollable?: boolean;
}
export function DashboardContent({
children,
isScrollable = true,
}: DashboardContentProps) {
return cloneElement(children, {
className: clsx(
children.props.className,
isScrollable && 'overflow-y-auto stable-scrollbar',
'dashboard-grid-content'
),
});
}

View File

@@ -0,0 +1,17 @@
import {createContext} from 'react';
export type DashboardSidenavStatus = 'open' | 'closed' | 'compact';
export interface DashboardContextValue {
leftSidenavStatus: DashboardSidenavStatus;
setLeftSidenavStatus: (status: DashboardSidenavStatus) => void;
rightSidenavStatus: DashboardSidenavStatus;
setRightSidenavStatus: (status: DashboardSidenavStatus) => void;
isMobileMode: boolean | null;
leftSidenavCanBeCompact?: boolean;
name: string;
}
export const DashboardLayoutContext = createContext<DashboardContextValue>(
null!
);

View File

@@ -0,0 +1,121 @@
import {ComponentPropsWithoutRef, useCallback, useMemo} from 'react';
import {
DashboardLayoutContext,
DashboardSidenavStatus,
} from './dashboard-layout-context';
import {Underlay} from '../overlays/underlay';
import {AnimatePresence} from 'framer-motion';
import {useControlledState} from '@react-stately/utils';
import {useMediaQuery} from '../../utils/hooks/use-media-query';
import {
getFromLocalStorage,
setInLocalStorage,
} from '../../utils/hooks/local-storage';
import {useBlockBodyOverflow} from '../../utils/hooks/use-block-body-overflow';
import clsx from 'clsx';
interface DashboardLayoutProps extends ComponentPropsWithoutRef<'div'> {
name: string;
leftSidenavCanBeCompact?: boolean;
leftSidenavStatus?: DashboardSidenavStatus;
onLeftSidenavChange?: (status: DashboardSidenavStatus) => void;
rightSidenavStatus?: DashboardSidenavStatus;
initialRightSidenavStatus?: DashboardSidenavStatus;
onRightSidenavChange?: (status: DashboardSidenavStatus) => void;
height?: string;
gridClassName?: string;
blockBodyOverflow?: boolean;
}
export function DashboardLayout({
children,
leftSidenavStatus: leftSidenav,
onLeftSidenavChange,
rightSidenavStatus: rightSidenav,
initialRightSidenavStatus,
onRightSidenavChange,
name,
leftSidenavCanBeCompact,
height = 'h-screen',
className,
gridClassName = 'dashboard-grid',
blockBodyOverflow = true,
...domProps
}: DashboardLayoutProps) {
useBlockBodyOverflow(!blockBodyOverflow);
const isMobile = useMediaQuery('(max-width: 1024px)');
const isCompactModeInitially = useMemo(() => {
return !name ? false : getFromLocalStorage(`${name}.sidenav.compact`);
}, [name]);
const defaultLeftSidenavStatus = isCompactModeInitially ? 'compact' : 'open';
const [leftSidenavStatus, setLeftSidenavStatus] = useControlledState(
leftSidenav,
isMobile ? 'closed' : defaultLeftSidenavStatus,
onLeftSidenavChange,
);
const rightSidenavStatusDefault = useMemo(() => {
if (isMobile) {
return 'closed';
}
if (initialRightSidenavStatus != null) {
return initialRightSidenavStatus;
}
const userSelected = getFromLocalStorage(
`${name}.sidenav.right.position`,
'open',
);
if (userSelected != null) {
return userSelected;
}
return initialRightSidenavStatus || 'closed';
}, [isMobile, name, initialRightSidenavStatus]);
const [rightSidenavStatus, _setRightSidenavStatus] = useControlledState(
rightSidenav,
rightSidenavStatusDefault,
onRightSidenavChange,
);
const setRightSidenavStatus = useCallback(
(status: DashboardSidenavStatus) => {
_setRightSidenavStatus(status);
setInLocalStorage(`${name}.sidenav.right.position`, status);
},
[_setRightSidenavStatus, name],
);
const shouldShowUnderlay =
isMobile && (leftSidenavStatus === 'open' || rightSidenavStatus === 'open');
return (
<DashboardLayoutContext.Provider
value={{
leftSidenavStatus,
setLeftSidenavStatus,
rightSidenavStatus,
setRightSidenavStatus,
leftSidenavCanBeCompact,
name,
isMobileMode: isMobile,
}}
>
<div
{...domProps}
className={clsx('relative isolate', gridClassName, className, height)}
>
{children}
<AnimatePresence>
{shouldShowUnderlay && (
<Underlay
position="fixed"
key="dashboard-underlay"
onClick={() => {
setLeftSidenavStatus('closed');
setRightSidenavStatus('closed');
}}
/>
)}
</AnimatePresence>
</div>
</DashboardLayoutContext.Provider>
);
}

View File

@@ -0,0 +1,67 @@
import {Navbar, NavbarProps} from '../navigation/navbar/navbar';
import {IconButton} from '../buttons/icon-button';
import React, {useContext} from 'react';
import clsx from 'clsx';
import {DashboardLayoutContext} from './dashboard-layout-context';
import {setInLocalStorage} from '../../utils/hooks/local-storage';
import {MenuOpenIcon} from '@common/icons/material/MenuOpen';
export interface DashboardNavbarProps
extends Omit<NavbarProps, 'toggleButton'> {
hideToggleButton?: boolean;
}
export function DashboardNavbar({
children,
className,
hideToggleButton,
...props
}: DashboardNavbarProps) {
const {
isMobileMode,
leftSidenavStatus,
setLeftSidenavStatus,
name,
leftSidenavCanBeCompact,
} = useContext(DashboardLayoutContext);
const shouldToggleCompactMode = leftSidenavCanBeCompact && !isMobileMode;
const shouldShowToggle =
!hideToggleButton && (isMobileMode || leftSidenavCanBeCompact);
const handleToggle = () => {
setLeftSidenavStatus(leftSidenavStatus === 'open' ? 'closed' : 'open');
};
const handleCompactModeToggle = () => {
const newStatus = leftSidenavStatus === 'compact' ? 'open' : 'compact';
setInLocalStorage(`${name}.sidenav.compact`, newStatus === 'compact');
setLeftSidenavStatus(newStatus);
};
return (
<Navbar
className={clsx('dashboard-grid-navbar', className)}
border="border-b"
size="sm"
toggleButton={
shouldShowToggle ? (
<IconButton
size="md"
onClick={() => {
if (shouldToggleCompactMode) {
handleCompactModeToggle();
} else {
handleToggle();
}
}}
>
<MenuOpenIcon />
</IconButton>
) : undefined
}
{...props}
>
{children}
</Navbar>
);
}

View File

@@ -0,0 +1,112 @@
import clsx from 'clsx';
import {m} from 'framer-motion';
import {cloneElement, ReactElement, useContext} from 'react';
import {DashboardLayoutContext} from './dashboard-layout-context';
export interface DashboardSidenavChildrenProps {
className?: string;
isCompactMode?: boolean;
}
export interface SidenavProps {
className?: string;
children: ReactElement<DashboardSidenavChildrenProps>;
position?: 'left' | 'right';
size?: 'sm' | 'md' | 'lg' | string;
mode?: 'overlay';
// absolute will place sidenav between navbar/footer, fixed will overlay it over nav/footer.
overlayPosition?: 'absolute' | 'fixed';
display?: 'flex' | 'block';
overflow?: string;
forceClosed?: boolean;
}
export function DashboardSidenav({
className,
position,
children,
size = 'md',
mode,
overlayPosition = 'fixed',
display = 'flex',
overflow = 'overflow-hidden',
forceClosed = false,
}: SidenavProps) {
const {
isMobileMode,
leftSidenavStatus,
setLeftSidenavStatus,
rightSidenavStatus,
setRightSidenavStatus,
} = useContext(DashboardLayoutContext);
const status = position === 'left' ? leftSidenavStatus : rightSidenavStatus;
const isOverlayMode = isMobileMode || mode === 'overlay';
const variants = {
open: {display, width: null as any},
compact: {
display,
width: null as any,
},
closed: {
width: 0,
transitionEnd: {
display: 'none',
},
},
};
const sizeClassName = getSize(status === 'compact' ? 'compact' : size);
return (
<m.div
variants={variants}
initial={false}
animate={forceClosed ? 'closed' : status}
transition={{type: 'tween', duration: 0.15}}
onClick={e => {
// close sidenav when user clicks a link or button on mobile
const target = e.target as HTMLElement;
if (isMobileMode && (target.closest('button') || target.closest('a'))) {
setLeftSidenavStatus('closed');
setRightSidenavStatus('closed');
}
}}
className={clsx(
className,
position === 'left'
? 'dashboard-grid-sidenav-left'
: 'dashboard-grid-sidenav-right',
'will-change-[width]',
overflow,
sizeClassName,
isOverlayMode && `${overlayPosition} bottom-0 top-0 z-20 shadow-2xl`,
isOverlayMode && position === 'left' && 'left-0',
isOverlayMode && position === 'right' && 'right-0',
)}
>
{cloneElement<DashboardSidenavChildrenProps>(children, {
className: clsx(
children.props.className,
'w-full h-full',
status === 'compact' && 'compact-scrollbar',
),
isCompactMode: status === 'compact',
})}
</m.div>
);
}
function getSize(size: SidenavProps['size'] | 'compact'): string {
switch (size) {
case 'compact':
return 'w-80';
case 'sm':
return 'w-224';
case 'md':
return 'w-240';
case 'lg':
return 'w-288';
default:
return size || '';
}
}