init commit,

This commit is contained in:
louiscklaw
2025-05-28 09:55:51 +08:00
commit efe70ceb69
8042 changed files with 951668 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
export * from './nav-section-vertical';
export { NavItem as NavSectionVerticalItem } from './nav-item';

View File

@@ -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),
}));

View File

@@ -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>
);
}

View File

@@ -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>
);
}