init commit,
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export * from './nav-section-vertical';
|
||||
|
||||
export { NavItem as NavSectionVerticalItem } from './nav-item';
|
@@ -0,0 +1,244 @@
|
||||
import type { CSSObject } from '@mui/material/styles';
|
||||
|
||||
import { mergeClasses } from 'minimal-shared/utils';
|
||||
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
|
||||
import { Iconify } from '../../iconify';
|
||||
import { createNavItem } from '../utils';
|
||||
import { navItemStyles, navSectionClasses } from '../styles';
|
||||
|
||||
import type { NavItemProps } from '../types';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function NavItem({
|
||||
path,
|
||||
icon,
|
||||
info,
|
||||
title,
|
||||
caption,
|
||||
/********/
|
||||
open,
|
||||
active,
|
||||
disabled,
|
||||
/********/
|
||||
depth,
|
||||
render,
|
||||
hasChild,
|
||||
slotProps,
|
||||
className,
|
||||
externalLink,
|
||||
enabledRootRedirect,
|
||||
...other
|
||||
}: NavItemProps) {
|
||||
const navItem = createNavItem({
|
||||
path,
|
||||
icon,
|
||||
info,
|
||||
depth,
|
||||
render,
|
||||
hasChild,
|
||||
externalLink,
|
||||
enabledRootRedirect,
|
||||
});
|
||||
|
||||
const ownerState: StyledState = {
|
||||
open,
|
||||
active,
|
||||
disabled,
|
||||
variant: navItem.rootItem ? 'rootItem' : 'subItem',
|
||||
};
|
||||
|
||||
return (
|
||||
<ItemRoot
|
||||
aria-label={title}
|
||||
{...ownerState}
|
||||
{...navItem.baseProps}
|
||||
className={mergeClasses([navSectionClasses.item.root, className], {
|
||||
[navSectionClasses.state.open]: open,
|
||||
[navSectionClasses.state.active]: active,
|
||||
[navSectionClasses.state.disabled]: disabled,
|
||||
})}
|
||||
sx={slotProps?.sx}
|
||||
{...other}
|
||||
>
|
||||
{icon && (
|
||||
<ItemIcon {...ownerState} className={navSectionClasses.item.icon} sx={slotProps?.icon}>
|
||||
{navItem.renderIcon}
|
||||
</ItemIcon>
|
||||
)}
|
||||
|
||||
{title && (
|
||||
<ItemTexts {...ownerState} className={navSectionClasses.item.texts} sx={slotProps?.texts}>
|
||||
<ItemTitle {...ownerState} className={navSectionClasses.item.title} sx={slotProps?.title}>
|
||||
{title}
|
||||
</ItemTitle>
|
||||
|
||||
{caption && (
|
||||
<Tooltip title={caption} placement="top-start">
|
||||
<ItemCaptionText
|
||||
{...ownerState}
|
||||
className={navSectionClasses.item.caption}
|
||||
sx={slotProps?.caption}
|
||||
>
|
||||
{caption}
|
||||
</ItemCaptionText>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ItemTexts>
|
||||
)}
|
||||
|
||||
{info && (
|
||||
<ItemInfo {...ownerState} className={navSectionClasses.item.info} sx={slotProps?.info}>
|
||||
{navItem.renderInfo}
|
||||
</ItemInfo>
|
||||
)}
|
||||
|
||||
{hasChild && (
|
||||
<ItemArrow
|
||||
{...ownerState}
|
||||
icon={open ? 'eva:arrow-ios-downward-fill' : 'eva:arrow-ios-forward-fill'}
|
||||
className={navSectionClasses.item.arrow}
|
||||
sx={slotProps?.arrow}
|
||||
/>
|
||||
)}
|
||||
</ItemRoot>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type StyledState = Pick<NavItemProps, 'open' | 'active' | 'disabled'> & {
|
||||
variant: 'rootItem' | 'subItem';
|
||||
};
|
||||
|
||||
const shouldForwardProp = (prop: string) =>
|
||||
!['open', 'active', 'disabled', 'variant', 'sx'].includes(prop);
|
||||
|
||||
/**
|
||||
* @slot root
|
||||
*/
|
||||
const ItemRoot = styled(ButtonBase, { shouldForwardProp })<StyledState>(({
|
||||
active,
|
||||
open,
|
||||
theme,
|
||||
}) => {
|
||||
const bulletSvg = `"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' viewBox='0 0 14 14'%3E%3Cpath d='M1 1v4a8 8 0 0 0 8 8h4' stroke='%23efefef' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"`;
|
||||
|
||||
const bulletStyles: CSSObject = {
|
||||
left: 0,
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
width: 'var(--nav-bullet-size)',
|
||||
height: 'var(--nav-bullet-size)',
|
||||
backgroundColor: 'var(--nav-bullet-light-color)',
|
||||
mask: `url(${bulletSvg}) no-repeat 50% 50%/100% auto`,
|
||||
WebkitMask: `url(${bulletSvg}) no-repeat 50% 50%/100% auto`,
|
||||
transform:
|
||||
theme.direction === 'rtl'
|
||||
? 'translate(calc(var(--nav-bullet-size) * 1), calc(var(--nav-bullet-size) * -0.4)) scaleX(-1)'
|
||||
: 'translate(calc(var(--nav-bullet-size) * -1), calc(var(--nav-bullet-size) * -0.4))',
|
||||
...theme.applyStyles('dark', {
|
||||
backgroundColor: 'var(--nav-bullet-dark-color)',
|
||||
}),
|
||||
};
|
||||
|
||||
const rootItemStyles: CSSObject = {
|
||||
minHeight: 'var(--nav-item-root-height)',
|
||||
...(open && {
|
||||
color: 'var(--nav-item-root-open-color)',
|
||||
backgroundColor: 'var(--nav-item-root-open-bg)',
|
||||
}),
|
||||
...(active && {
|
||||
color: 'var(--nav-item-root-active-color)',
|
||||
backgroundColor: 'var(--nav-item-root-active-bg)',
|
||||
'&:hover': { backgroundColor: 'var(--nav-item-root-active-hover-bg)' },
|
||||
...theme.applyStyles('dark', {
|
||||
color: 'var(--nav-item-root-active-color-on-dark)',
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
const subItemStyles: CSSObject = {
|
||||
minHeight: 'var(--nav-item-sub-height)',
|
||||
'&::before': bulletStyles,
|
||||
...(open && {
|
||||
color: 'var(--nav-item-sub-open-color)',
|
||||
backgroundColor: 'var(--nav-item-sub-open-bg)',
|
||||
}),
|
||||
...(active && {
|
||||
color: 'var(--nav-item-sub-active-color)',
|
||||
backgroundColor: 'var(--nav-item-sub-active-bg)',
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
width: '100%',
|
||||
paddingTop: 'var(--nav-item-pt)',
|
||||
paddingLeft: 'var(--nav-item-pl)',
|
||||
paddingRight: 'var(--nav-item-pr)',
|
||||
paddingBottom: 'var(--nav-item-pb)',
|
||||
borderRadius: 'var(--nav-item-radius)',
|
||||
color: 'var(--nav-item-color)',
|
||||
'&:hover': { backgroundColor: 'var(--nav-item-hover-bg)' },
|
||||
variants: [
|
||||
{ props: { variant: 'rootItem' }, style: rootItemStyles },
|
||||
{ props: { variant: 'subItem' }, style: subItemStyles },
|
||||
{ props: { disabled: true }, style: navItemStyles.disabled },
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* @slot icon
|
||||
*/
|
||||
const ItemIcon = styled('span', { shouldForwardProp })<StyledState>(() => ({
|
||||
...navItemStyles.icon,
|
||||
width: 'var(--nav-icon-size)',
|
||||
height: 'var(--nav-icon-size)',
|
||||
margin: 'var(--nav-icon-margin)',
|
||||
}));
|
||||
|
||||
/**
|
||||
* @slot texts
|
||||
*/
|
||||
const ItemTexts = styled('span', { shouldForwardProp })<StyledState>(() => ({
|
||||
...navItemStyles.texts,
|
||||
}));
|
||||
|
||||
/**
|
||||
* @slot title
|
||||
*/
|
||||
const ItemTitle = styled('span', { shouldForwardProp })<StyledState>(({ theme }) => ({
|
||||
...navItemStyles.title(theme),
|
||||
...theme.typography.body2,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
variants: [
|
||||
{ props: { active: true }, style: { fontWeight: theme.typography.fontWeightSemiBold } },
|
||||
],
|
||||
}));
|
||||
|
||||
/**
|
||||
* @slot caption text
|
||||
*/
|
||||
const ItemCaptionText = styled('span', { shouldForwardProp })<StyledState>(({ theme }) => ({
|
||||
...navItemStyles.captionText(theme),
|
||||
color: 'var(--nav-item-caption-color)',
|
||||
}));
|
||||
|
||||
/**
|
||||
* @slot info
|
||||
*/
|
||||
const ItemInfo = styled('span', { shouldForwardProp })<StyledState>(({ theme }) => ({
|
||||
...navItemStyles.info,
|
||||
}));
|
||||
|
||||
/**
|
||||
* @slot arrow
|
||||
*/
|
||||
const ItemArrow = styled(Iconify, { shouldForwardProp })<StyledState>(({ theme }) => ({
|
||||
...navItemStyles.arrow(theme),
|
||||
}));
|
@@ -0,0 +1,128 @@
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
import { isActiveLink, isExternalLink } from 'minimal-shared/utils';
|
||||
|
||||
import { usePathname } from 'src/routes/hooks';
|
||||
|
||||
import { NavItem } from './nav-item';
|
||||
import { navSectionClasses } from '../styles';
|
||||
import { NavUl, NavLi, NavCollapse } from '../components';
|
||||
|
||||
import type { NavListProps, NavSubListProps } from '../types';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function NavList({
|
||||
data,
|
||||
depth,
|
||||
render,
|
||||
slotProps,
|
||||
checkPermissions,
|
||||
enabledRootRedirect,
|
||||
}: NavListProps) {
|
||||
const pathname = usePathname();
|
||||
const navItemRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const isActive = isActiveLink(pathname, data.path, !!data.children);
|
||||
|
||||
const { value: open, onFalse: onClose, onToggle } = useBoolean(isActive);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
onClose();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname]);
|
||||
|
||||
const handleToggleMenu = useCallback(() => {
|
||||
if (data.children) {
|
||||
onToggle();
|
||||
}
|
||||
}, [data.children, onToggle]);
|
||||
|
||||
const renderNavItem = () => (
|
||||
<NavItem
|
||||
ref={navItemRef}
|
||||
// slots
|
||||
path={data.path}
|
||||
icon={data.icon}
|
||||
info={data.info}
|
||||
title={data.title}
|
||||
caption={data.caption}
|
||||
// state
|
||||
open={open}
|
||||
active={isActive}
|
||||
disabled={data.disabled}
|
||||
// options
|
||||
depth={depth}
|
||||
render={render}
|
||||
hasChild={!!data.children}
|
||||
externalLink={isExternalLink(data.path)}
|
||||
enabledRootRedirect={enabledRootRedirect}
|
||||
// styles
|
||||
slotProps={depth === 1 ? slotProps?.rootItem : slotProps?.subItem}
|
||||
// actions
|
||||
onClick={handleToggleMenu}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderCollapse = () =>
|
||||
!!data.children && (
|
||||
<NavCollapse mountOnEnter unmountOnExit depth={depth} in={open} data-group={data.title}>
|
||||
<NavSubList
|
||||
data={data.children}
|
||||
render={render}
|
||||
depth={depth}
|
||||
slotProps={slotProps}
|
||||
checkPermissions={checkPermissions}
|
||||
enabledRootRedirect={enabledRootRedirect}
|
||||
/>
|
||||
</NavCollapse>
|
||||
);
|
||||
|
||||
// Hidden item by role
|
||||
if (data.allowedRoles && checkPermissions && checkPermissions(data.allowedRoles)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLi
|
||||
disabled={data.disabled}
|
||||
sx={{
|
||||
...(!!data.children && {
|
||||
[`& .${navSectionClasses.li}`]: { '&:first-of-type': { mt: 'var(--nav-item-gap)' } },
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{renderNavItem()}
|
||||
{renderCollapse()}
|
||||
</NavLi>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
function NavSubList({
|
||||
data,
|
||||
render,
|
||||
depth = 0,
|
||||
slotProps,
|
||||
checkPermissions,
|
||||
enabledRootRedirect,
|
||||
}: NavSubListProps) {
|
||||
return (
|
||||
<NavUl sx={{ gap: 'var(--nav-item-gap)' }}>
|
||||
{data.map((list) => (
|
||||
<NavList
|
||||
key={list.title}
|
||||
data={list}
|
||||
render={render}
|
||||
depth={depth + 1}
|
||||
slotProps={slotProps}
|
||||
checkPermissions={checkPermissions}
|
||||
enabledRootRedirect={enabledRootRedirect}
|
||||
/>
|
||||
))}
|
||||
</NavUl>
|
||||
);
|
||||
}
|
@@ -0,0 +1,101 @@
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { mergeClasses } from 'minimal-shared/utils';
|
||||
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
import { NavList } from './nav-list';
|
||||
import { Nav, NavUl, NavLi, NavSubheader } from '../components';
|
||||
import { navSectionClasses, navSectionCssVars } from '../styles';
|
||||
|
||||
import type { NavGroupProps, NavSectionProps } from '../types';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function NavSectionVertical({
|
||||
sx,
|
||||
data,
|
||||
render,
|
||||
className,
|
||||
slotProps,
|
||||
checkPermissions,
|
||||
enabledRootRedirect,
|
||||
cssVars: overridesVars,
|
||||
...other
|
||||
}: NavSectionProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
const cssVars = { ...navSectionCssVars.vertical(theme), ...overridesVars };
|
||||
|
||||
return (
|
||||
<Nav
|
||||
className={mergeClasses([navSectionClasses.vertical, className])}
|
||||
sx={[{ ...cssVars }, ...(Array.isArray(sx) ? sx : [sx])]}
|
||||
{...other}
|
||||
>
|
||||
<NavUl sx={{ flex: '1 1 auto', gap: 'var(--nav-item-gap)' }}>
|
||||
{data.map((group) => (
|
||||
<Group
|
||||
key={group.subheader ?? group.items[0].title}
|
||||
subheader={group.subheader}
|
||||
items={group.items}
|
||||
render={render}
|
||||
slotProps={slotProps}
|
||||
checkPermissions={checkPermissions}
|
||||
enabledRootRedirect={enabledRootRedirect}
|
||||
/>
|
||||
))}
|
||||
</NavUl>
|
||||
</Nav>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
function Group({
|
||||
items,
|
||||
render,
|
||||
subheader,
|
||||
slotProps,
|
||||
checkPermissions,
|
||||
enabledRootRedirect,
|
||||
}: NavGroupProps) {
|
||||
const groupOpen = useBoolean(true);
|
||||
|
||||
const renderContent = () => (
|
||||
<NavUl sx={{ gap: 'var(--nav-item-gap)' }}>
|
||||
{items.map((list) => (
|
||||
<NavList
|
||||
key={list.title}
|
||||
data={list}
|
||||
render={render}
|
||||
depth={1}
|
||||
slotProps={slotProps}
|
||||
checkPermissions={checkPermissions}
|
||||
enabledRootRedirect={enabledRootRedirect}
|
||||
/>
|
||||
))}
|
||||
</NavUl>
|
||||
);
|
||||
|
||||
return (
|
||||
<NavLi>
|
||||
{subheader ? (
|
||||
<>
|
||||
<NavSubheader
|
||||
data-title={subheader}
|
||||
open={groupOpen.value}
|
||||
onClick={groupOpen.onToggle}
|
||||
sx={slotProps?.subheader}
|
||||
>
|
||||
{subheader}
|
||||
</NavSubheader>
|
||||
|
||||
<Collapse in={groupOpen.value}>{renderContent()}</Collapse>
|
||||
</>
|
||||
) : (
|
||||
renderContent()
|
||||
)}
|
||||
</NavLi>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user