code: tsx,nextjs,frontends (4 files)

This commit is contained in:
Richard Ward
2025-12-30 18:05:59 +00:00
parent 1b70cc0834
commit 8ab2e225ed
4 changed files with 136 additions and 197 deletions

View File

@@ -1,27 +1,32 @@
'use client'
import { Box, Breadcrumbs as MuiBreadcrumbs, Link, Typography } from '@mui/material'
import { Box, Breadcrumbs, Link, Typography } from '@/fakemui'
import { forwardRef, ReactNode } from 'react'
import { MoreHoriz, NavigateNext } from '@/fakemui/icons'
import styles from './Breadcrumb.module.scss'
interface BreadcrumbProps {
children: ReactNode
className?: string
}
const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>(({ children, ...props }, ref) => {
return (
<MuiBreadcrumbs
ref={ref}
separator={<NavigateNext size={16} />}
aria-label="breadcrumb"
{...props}
>
{children}
</MuiBreadcrumbs>
)
})
const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>(
({ children, className = '', ...props }, ref) => {
return (
<Breadcrumbs
ref={ref}
separator={<NavigateNext size={16} />}
className={`${styles.breadcrumbs} ${className}`}
aria-label="breadcrumb"
{...props}
>
{children}
</Breadcrumbs>
)
}
)
Breadcrumb.displayName = 'Breadcrumb'
interface BreadcrumbListProps {
@@ -30,7 +35,7 @@ interface BreadcrumbListProps {
}
const BreadcrumbList = forwardRef<HTMLOListElement, BreadcrumbListProps>(
({ children, ...props }, ref) => {
({ children, className = '', ...props }, ref) => {
return <>{children}</>
}
)
@@ -42,9 +47,9 @@ interface BreadcrumbItemProps {
}
const BreadcrumbItem = forwardRef<HTMLLIElement, BreadcrumbItemProps>(
({ children, ...props }, ref) => {
({ children, className = '', ...props }, ref) => {
return (
<Box component="span" ref={ref} {...props}>
<Box component="span" ref={ref} className={`${styles.breadcrumbItem} ${className}`} {...props}>
{children}
</Box>
)
@@ -60,14 +65,14 @@ interface BreadcrumbLinkProps {
}
const BreadcrumbLink = forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(
({ children, href, ...props }, ref) => {
({ children, href, className = '', ...props }, ref) => {
return (
<Link
ref={ref}
href={href}
underline="hover"
color="inherit"
sx={{ display: 'flex', alignItems: 'center', fontSize: '0.875rem' }}
className={`${styles.breadcrumbLink} ${className}`}
{...props}
>
{children}
@@ -83,12 +88,11 @@ interface BreadcrumbPageProps {
}
const BreadcrumbPage = forwardRef<HTMLSpanElement, BreadcrumbPageProps>(
({ children, ...props }, ref) => {
({ children, className = '', ...props }, ref) => {
return (
<Typography
ref={ref}
color="text.primary"
sx={{ fontSize: '0.875rem', fontWeight: 500 }}
className={`${styles.breadcrumbPage} ${className}`}
{...props}
>
{children}
@@ -103,10 +107,10 @@ interface BreadcrumbSeparatorProps {
className?: string
}
const BreadcrumbSeparator = ({ children, ...props }: BreadcrumbSeparatorProps) => {
const BreadcrumbSeparator = ({ children, className = '', ...props }: BreadcrumbSeparatorProps) => {
return (
<Box component="span" sx={{ mx: 1, color: 'text.secondary' }} {...props}>
{children || <NavigateNextIcon fontSize="small" />}
<Box component="span" className={`${styles.breadcrumbSeparator} ${className}`} {...props}>
{children || <NavigateNext size={16} />}
</Box>
)
}
@@ -116,13 +120,11 @@ interface BreadcrumbEllipsisProps {
className?: string
}
const BreadcrumbEllipsis = ({ ...props }: BreadcrumbEllipsisProps) => {
const BreadcrumbEllipsis = ({ className = '', ...props }: BreadcrumbEllipsisProps) => {
return (
<Box component="span" sx={{ display: 'flex', alignItems: 'center' }} {...props}>
<MoreHorizIcon fontSize="small" />
<Box component="span" sx={{ position: 'absolute', width: 1, height: 1, overflow: 'hidden' }}>
More
</Box>
<Box component="span" className={`${styles.breadcrumbEllipsis} ${className}`} {...props}>
<MoreHoriz size={16} />
<span className={styles.srOnly}>More</span>
</Box>
)
}

View File

@@ -1,19 +1,12 @@
'use client'
import {
Box,
Collapse,
Divider,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
} from '@mui/material'
import { Box, Collapse, Divider, List, ListItem, ListItemIcon, ListItemText } from '@/fakemui'
import { forwardRef, ReactNode, useState } from 'react'
import { ExpandLess, ExpandMore } from '@/fakemui/icons'
import styles from './NavGroup.module.scss'
export interface NavGroupProps {
label: string
icon?: ReactNode
@@ -26,7 +19,7 @@ export interface NavGroupProps {
const NavGroup = forwardRef<HTMLDivElement, NavGroupProps>(
(
{ label, icon, children, defaultOpen = false, disabled = false, divider = false, ...props },
{ label, icon, children, defaultOpen = false, disabled = false, divider = false, className = '', ...props },
ref
) => {
const [open, setOpen] = useState(defaultOpen)
@@ -37,51 +30,44 @@ const NavGroup = forwardRef<HTMLDivElement, NavGroupProps>(
}
}
const buttonClasses = [styles.groupButton, disabled && styles.groupButtonDisabled]
.filter(Boolean)
.join(' ')
const collapseClasses = [styles.collapse, open && styles.collapseOpen].filter(Boolean).join(' ')
const childListClasses = [
styles.childList,
icon ? styles.childListWithIcon : styles.childListNoIcon,
]
.filter(Boolean)
.join(' ')
return (
<Box ref={ref} {...props}>
{divider && <Divider sx={{ my: 1 }} />}
<ListItem disablePadding>
<ListItemButton
<Box ref={ref} className={`${styles.navGroup} ${className}`} {...props}>
{divider && <Divider className={styles.divider} />}
<ListItem className={styles.groupItem}>
<button
type="button"
className={buttonClasses}
onClick={handleToggle}
disabled={disabled}
sx={{
borderRadius: 1,
mx: 0.5,
my: 0.25,
'&:hover': {
bgcolor: 'action.hover',
},
}}
>
{icon && (
<ListItemIcon
sx={{
minWidth: 40,
color: 'text.secondary',
}}
>
{icon}
</ListItemIcon>
)}
{icon && <ListItemIcon className={styles.icon}>{icon}</ListItemIcon>}
<ListItemText
primary={label}
primaryTypographyProps={{
variant: 'body2',
fontWeight: 600,
color: 'text.primary',
}}
primary={<span className={styles.labelText}>{label}</span>}
/>
{open ? (
<ExpandLess size={16} style={{ color: 'rgba(0,0,0,0.54)' }} />
) : (
<ExpandMore size={16} style={{ color: 'rgba(0,0,0,0.54)' }} />
)}
</ListItemButton>
<span className={styles.expandIcon}>
{open ? (
<ExpandLess size={16} />
) : (
<ExpandMore size={16} />
)}
</span>
</button>
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding sx={{ pl: icon ? 3 : 1 }}>
{children}
</List>
<Collapse in={open} className={collapseClasses}>
<List className={childListClasses}>{children}</List>
</Collapse>
</Box>
)

View File

@@ -1,17 +1,11 @@
'use client'
import {
Badge,
Box,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
type ListItemProps,
} from '@mui/material'
import { ListItem, ListItemIcon, ListItemText } from '@/fakemui'
import { forwardRef, ReactNode } from 'react'
export interface NavItemProps extends Omit<ListItemProps, 'children'> {
import styles from './NavItem.module.scss'
export interface NavItemProps extends React.LiHTMLAttributes<HTMLLIElement> {
icon?: ReactNode
label: string
onClick?: () => void
@@ -25,6 +19,16 @@ export interface NavItemProps extends Omit<ListItemProps, 'children'> {
className?: string
}
const badgeColorMap: Record<string, string> = {
default: styles.badgeDefault,
primary: styles.badgePrimary,
secondary: styles.badgeSecondary,
error: styles.badgeError,
warning: styles.badgeWarning,
info: styles.badgeInfo,
success: styles.badgeSuccess,
}
const NavItem = forwardRef<HTMLLIElement, NavItemProps>(
(
{
@@ -38,93 +42,61 @@ const NavItem = forwardRef<HTMLLIElement, NavItemProps>(
href,
secondaryLabel,
dense = false,
sx,
className = '',
...props
},
ref
) => {
const buttonClasses = [
styles.navItemButton,
active && styles.navItemButtonSelected,
disabled && styles.navItemButtonDisabled,
dense && styles.navItemButtonDense,
]
.filter(Boolean)
.join(' ')
const iconClasses = [styles.icon, active && styles.iconActive].filter(Boolean).join(' ')
const primaryTextClasses = [styles.textPrimary, active && styles.textPrimaryActive]
.filter(Boolean)
.join(' ')
const badgeClasses = [styles.badge, badgeColorMap[badgeColor]].filter(Boolean).join(' ')
const ButtonComponent = href ? 'a' : 'button'
return (
<ListItem
ref={ref}
disablePadding
{...props}
sx={sx}
>
<ListItemButton
<ListItem ref={ref} className={`${styles.navItem} ${className}`} {...props}>
<ButtonComponent
className={buttonClasses}
onClick={onClick}
disabled={disabled}
selected={active}
dense={dense}
href={href}
sx={{
borderRadius: 1,
mx: 0.5,
my: 0.25,
'&.Mui-selected': {
bgcolor: 'action.selected',
'&:hover': {
bgcolor: 'action.hover',
},
},
'&:hover': {
bgcolor: 'action.hover',
},
}}
{...(href ? { href } : { type: 'button' })}
>
{icon && (
<ListItemIcon
sx={{
minWidth: 40,
color: active ? 'primary.main' : 'text.secondary',
}}
>
<ListItemIcon className={iconClasses}>
{badge !== undefined ? (
<Badge
badgeContent={badge}
color={badgeColor}
sx={{
'& .MuiBadge-badge': {
fontSize: '0.625rem',
height: 16,
minWidth: 16,
padding: '0 4px',
},
}}
>
<span className={styles.badgeWrapper}>
{icon}
</Badge>
<span className={badgeClasses}>{badge}</span>
</span>
) : (
icon
)}
</ListItemIcon>
)}
<ListItemText
primary={label}
secondary={secondaryLabel}
primaryTypographyProps={{
variant: 'body2',
fontWeight: active ? 600 : 400,
color: active ? 'primary.main' : 'text.primary',
}}
secondaryTypographyProps={{
variant: 'caption',
}}
className={styles.text}
primary={<span className={primaryTextClasses}>{label}</span>}
secondary={secondaryLabel && <span className={styles.textSecondary}>{secondaryLabel}</span>}
/>
{badge !== undefined && !icon && (
<Box sx={{ ml: 1 }}>
<Badge
badgeContent={badge}
color={badgeColor}
sx={{
'& .MuiBadge-badge': {
position: 'static',
transform: 'none',
},
}}
/>
</Box>
<span className={styles.badgeWrapper}>
<span className={`${badgeClasses} ${styles.badgeStatic}`}>{badge}</span>
</span>
)}
</ListItemButton>
</ButtonComponent>
</ListItem>
)
}

View File

@@ -1,9 +1,11 @@
'use client'
import { Box, Link as MuiLink, LinkProps as MuiLinkProps } from '@mui/material'
import { Box, Link } from '@/fakemui'
import { forwardRef, ReactNode } from 'react'
export interface NavLinkProps extends Omit<MuiLinkProps, 'component'> {
import styles from './NavLink.module.scss'
export interface NavLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
href: string
active?: boolean
disabled?: boolean
@@ -13,56 +15,33 @@ export interface NavLinkProps extends Omit<MuiLinkProps, 'component'> {
}
const NavLink = forwardRef<HTMLAnchorElement, NavLinkProps>(
({ href, active = false, disabled = false, children, icon, sx, ...props }, ref) => {
({ href, active = false, disabled = false, children, icon, className = '', ...props }, ref) => {
const linkClasses = [
styles.navLink,
active && styles.navLinkActive,
disabled && styles.navLinkDisabled,
className,
]
.filter(Boolean)
.join(' ')
const iconClasses = [styles.icon, active && styles.iconActive].filter(Boolean).join(' ')
return (
<MuiLink
<Link
ref={ref}
href={disabled ? undefined : href}
underline="none"
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
px: 2,
py: 1,
borderRadius: 1,
fontSize: '0.875rem',
fontWeight: active ? 600 : 500,
color: active ? 'primary.main' : 'text.primary',
bgcolor: active ? 'action.selected' : 'transparent',
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1,
transition: 'background-color 0.2s, color 0.2s',
'&:hover': disabled
? {}
: {
bgcolor: active ? 'action.selected' : 'action.hover',
color: active ? 'primary.main' : 'text.primary',
},
'&:focus-visible': {
outline: '2px solid',
outlineColor: 'primary.main',
outlineOffset: 2,
},
...sx,
}}
className={linkClasses}
{...props}
>
{icon && (
<Box
component="span"
sx={{
display: 'flex',
alignItems: 'center',
fontSize: '1.25rem',
color: active ? 'primary.main' : 'text.secondary',
}}
>
<Box component="span" className={iconClasses}>
{icon}
</Box>
)}
{children}
</MuiLink>
</Link>
)
}
)