code: tsx,nextjs,frontends (5 files)

This commit is contained in:
Richard Ward
2025-12-30 18:48:56 +00:00
parent 199a04954f
commit 031cb259af
5 changed files with 100 additions and 192 deletions

View File

@@ -1,6 +1,6 @@
import { Box, Card, InputAdornment, Stack, TextField, Typography } from '@mui/material'
import { useState } from 'react'
import { Box, Card, Stack, TextField, Typography } from '@/fakemui'
import { Search } from '@/fakemui/icons'
import { getComponentIcon } from '@/components/get-component-icon'
@@ -23,39 +23,22 @@ export function ComponentCatalog({ onDragStart }: ComponentCatalogProps) {
const categories = Array.from(new Set(componentCatalog.map(c => c.category)))
return (
<Box
sx={{
width: theme => theme.custom.sidebar.width,
bgcolor: 'background.paper',
color: 'text.primary',
display: 'flex',
flexDirection: 'column',
borderRight: 1,
borderColor: 'divider',
height: '100vh',
}}
>
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1.5 }}>
<Box className="component-catalog">
<Box className="component-catalog__header">
<Typography variant="h6" className="component-catalog__title">
Components
</Typography>
<TextField
placeholder="Search components..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
size="small"
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search style={{ color: 'rgba(0,0,0,0.54)', fontSize: 18 }} />
</InputAdornment>
),
}}
/>
<div className="component-catalog__search">
<Search className="component-catalog__search-icon" />
<TextField
placeholder="Search components..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
</Box>
<Box sx={{ flex: 1, overflowY: 'auto', p: 2 }}>
<Box className="component-catalog__content">
<Stack spacing={3}>
{categories.map(category => {
const categoryComponents = filteredComponents.filter(c => c.category === category)
@@ -63,55 +46,22 @@ export function ComponentCatalog({ onDragStart }: ComponentCatalogProps) {
return (
<Box key={category}>
<Typography
variant="overline"
sx={{
display: 'block',
fontWeight: 600,
letterSpacing: '0.08em',
color: 'text.secondary',
mb: 1.5,
}}
>
<Typography variant="overline" className="component-catalog__category-label">
{category}
</Typography>
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
gap: 1.5,
}}
>
<Box className="component-catalog__grid">
{categoryComponents.map(component => (
<Card
key={component.type}
draggable
onDragStart={() => onDragStart(component)}
sx={{
p: 1.5,
cursor: 'grab',
'&:active': { cursor: 'grabbing' },
transition:
'border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease',
'&:hover': {
borderColor: 'primary.main',
boxShadow: 3,
transform: 'translateY(-1px)',
},
}}
className="component-catalog__card"
>
<Stack spacing={1} alignItems="center" textAlign="center">
<Box
sx={{
color: 'primary.main',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{getComponentIcon(component.icon, { sx: { fontSize: 20 } })}
<Stack spacing={1} alignItems="center" className="text-center">
<Box className="component-catalog__icon">
{getComponentIcon(component.icon, { className: 'icon-sm' })}
</Box>
<Typography variant="caption" sx={{ fontWeight: 600 }}>
<Typography variant="caption" className="font-semibold">
{component.label}
</Typography>
</Stack>

View File

@@ -1,21 +1,11 @@
'use client'
import {
Box,
Divider,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
MenuItemProps,
MenuProps,
Typography,
} from '@mui/material'
import { forwardRef, MouseEvent as ReactMouseEvent, ReactNode } from 'react'
import { Box, Divider, ListItemIcon, ListItemText, Menu, MenuItem, Typography } from '@/fakemui'
import { Check, ChevronRight } from '@/fakemui/icons'
// DropdownMenu (uses MUI Menu under the hood)
// DropdownMenu (uses FakeMUI Menu under the hood)
export interface DropdownMenuProps {
children: ReactNode
open?: boolean
@@ -40,7 +30,7 @@ const DropdownMenuTrigger = forwardRef<HTMLDivElement, DropdownMenuTriggerProps>
<Box
ref={ref}
onClick={onClick}
sx={{ display: 'inline-flex', cursor: 'pointer' }}
className="dropdown-menu-trigger"
{...props}
>
{children}
@@ -51,33 +41,25 @@ const DropdownMenuTrigger = forwardRef<HTMLDivElement, DropdownMenuTriggerProps>
DropdownMenuTrigger.displayName = 'DropdownMenuTrigger'
// DropdownMenuContent
export interface DropdownMenuContentProps extends Omit<MenuProps, 'open'> {
export interface DropdownMenuContentProps {
children?: ReactNode
align?: 'start' | 'center' | 'end'
side?: 'top' | 'right' | 'bottom' | 'left'
sideOffset?: number
anchorEl?: HTMLElement | null
onClose?: () => void
className?: string
}
const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(
({ children, align = 'start', side = 'bottom', anchorEl, onClose, ...props }, ref) => {
const anchorOrigin = {
vertical: side === 'top' ? 'top' : 'bottom',
horizontal: align === 'end' ? 'right' : align === 'center' ? 'center' : 'left',
} as const
({ children, align = 'start', side = 'bottom', anchorEl, onClose, className, ...props }, ref) => {
return (
<Menu
ref={ref}
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={onClose}
anchorOrigin={anchorOrigin}
transformOrigin={{
vertical: side === 'top' ? 'bottom' : 'top',
horizontal: anchorOrigin.horizontal,
}}
PaperProps={{
sx: { minWidth: 160, borderRadius: 1, mt: 0.5 },
}}
className={`dropdown-menu-content ${className || ''}`}
{...props}
>
{children}
@@ -88,30 +70,28 @@ const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>
DropdownMenuContent.displayName = 'DropdownMenuContent'
// DropdownMenuItem
export interface DropdownMenuItemProps extends MenuItemProps {
export interface DropdownMenuItemProps {
children?: ReactNode
inset?: boolean
icon?: ReactNode
shortcut?: string
onClick?: () => void
disabled?: boolean
className?: string
}
const DropdownMenuItem = forwardRef<HTMLLIElement, DropdownMenuItemProps>(
({ children, inset, icon, shortcut, sx, ...props }, ref) => {
const DropdownMenuItem = forwardRef<HTMLButtonElement, DropdownMenuItemProps>(
({ children, inset, icon, shortcut, className, ...props }, ref) => {
return (
<MenuItem
ref={ref}
sx={{
py: 0.75,
px: 2,
fontSize: '0.875rem',
...(inset && { pl: 6 }),
...sx,
}}
className={`dropdown-menu-item ${inset ? 'dropdown-menu-item--inset' : ''} ${className || ''}`}
{...props}
>
{icon && <ListItemIcon sx={{ minWidth: 28 }}>{icon}</ListItemIcon>}
{icon && <ListItemIcon>{icon}</ListItemIcon>}
<ListItemText>{children}</ListItemText>
{shortcut && (
<Typography variant="caption" color="text.secondary" sx={{ ml: 2 }}>
<Typography variant="caption" className="dropdown-menu-shortcut">
{shortcut}
</Typography>
)}
@@ -122,22 +102,24 @@ const DropdownMenuItem = forwardRef<HTMLLIElement, DropdownMenuItemProps>(
DropdownMenuItem.displayName = 'DropdownMenuItem'
// DropdownMenuCheckboxItem
interface DropdownMenuCheckboxItemProps extends Omit<MenuItemProps, 'onChange'> {
interface DropdownMenuCheckboxItemProps {
children?: ReactNode
checked?: boolean
onCheckedChange?: (checked: boolean) => void
className?: string
}
const DropdownMenuCheckboxItem = forwardRef<HTMLLIElement, DropdownMenuCheckboxItemProps>(
({ children, checked, onCheckedChange, ...props }, ref) => {
const DropdownMenuCheckboxItem = forwardRef<HTMLButtonElement, DropdownMenuCheckboxItemProps>(
({ children, checked, onCheckedChange, className, ...props }, ref) => {
return (
<MenuItem
ref={ref}
onClick={() => onCheckedChange?.(!checked)}
sx={{ py: 0.75, px: 2 }}
className={`dropdown-menu-checkbox-item ${className || ''}`}
{...props}
>
<ListItemIcon sx={{ minWidth: 28 }}>
{checked && <CheckIcon fontSize="small" />}
<ListItemIcon>
{checked && <Check size={16} />}
</ListItemIcon>
<ListItemText>{children}</ListItemText>
</MenuItem>
@@ -150,18 +132,11 @@ DropdownMenuCheckboxItem.displayName = 'DropdownMenuCheckboxItem'
const DropdownMenuLabel = forwardRef<
HTMLDivElement,
{ children: ReactNode; inset?: boolean; className?: string }
>(({ children, inset, ...props }, ref) => {
>(({ children, inset, className, ...props }, ref) => {
return (
<Box
ref={ref}
sx={{
px: 2,
py: 0.75,
fontSize: '0.75rem',
fontWeight: 600,
color: 'text.secondary',
...(inset && { pl: 6 }),
}}
className={`dropdown-menu-label ${inset ? 'dropdown-menu-label--inset' : ''} ${className || ''}`}
{...props}
>
{children}
@@ -171,15 +146,15 @@ const DropdownMenuLabel = forwardRef<
DropdownMenuLabel.displayName = 'DropdownMenuLabel'
// DropdownMenuSeparator
const DropdownMenuSeparator = forwardRef<HTMLHRElement>((props, ref) => {
return <Divider ref={ref} sx={{ my: 0.5 }} {...props} />
const DropdownMenuSeparator = forwardRef<HTMLHRElement, { className?: string }>((props, ref) => {
return <Divider ref={ref} className="dropdown-menu-separator" {...props} />
})
DropdownMenuSeparator.displayName = 'DropdownMenuSeparator'
// DropdownMenuShortcut
const DropdownMenuShortcut = ({ children }: { children: ReactNode }) => {
return (
<Typography variant="caption" color="text.secondary" sx={{ ml: 'auto', pl: 2 }}>
<Typography variant="caption" className="dropdown-menu-shortcut">
{children}
</Typography>
)
@@ -188,9 +163,9 @@ DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
// DropdownMenuGroup
const DropdownMenuGroup = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
({ children, ...props }, ref) => {
({ children, className, ...props }, ref) => {
return (
<Box ref={ref} {...props}>
<Box ref={ref} className={`dropdown-menu-group ${className || ''}`} {...props}>
{children}
</Box>
)
@@ -204,13 +179,17 @@ DropdownMenuSub.displayName = 'DropdownMenuSub'
// DropdownMenuSubTrigger
const DropdownMenuSubTrigger = forwardRef<
HTMLLIElement,
HTMLButtonElement,
{ children: ReactNode; inset?: boolean; className?: string }
>(({ children, inset, ...props }, ref) => {
>(({ children, inset, className, ...props }, ref) => {
return (
<MenuItem ref={ref} sx={{ py: 0.75, px: 2, ...(inset && { pl: 6 }) }} {...props}>
<MenuItem
ref={ref}
className={`dropdown-menu-sub-trigger ${inset ? 'dropdown-menu-sub-trigger--inset' : ''} ${className || ''}`}
{...props}
>
<ListItemText>{children}</ListItemText>
<ChevronRightIcon fontSize="small" sx={{ ml: 1 }} />
<ChevronRight size={16} />
</MenuItem>
)
})
@@ -220,9 +199,9 @@ DropdownMenuSubTrigger.displayName = 'DropdownMenuSubTrigger'
const DropdownMenuSubContent = forwardRef<
HTMLDivElement,
{ children: ReactNode; className?: string }
>(({ children, ...props }, ref) => {
>(({ children, className, ...props }, ref) => {
return (
<Box ref={ref} {...props}>
<Box ref={ref} className={`dropdown-menu-sub-content ${className || ''}`} {...props}>
{children}
</Box>
)
@@ -242,9 +221,9 @@ const DropdownMenuRadioGroup = forwardRef<
onValueChange?: (value: string) => void
className?: string
}
>(({ children, ...props }, ref) => {
>(({ children, className, ...props }, ref) => {
return (
<Box ref={ref} {...props}>
<Box ref={ref} className={`dropdown-menu-radio-group ${className || ''}`} {...props}>
{children}
</Box>
)
@@ -253,13 +232,13 @@ DropdownMenuRadioGroup.displayName = 'DropdownMenuRadioGroup'
// DropdownMenuRadioItem
const DropdownMenuRadioItem = forwardRef<
HTMLLIElement,
HTMLButtonElement,
{ children: ReactNode; value: string; className?: string }
>(({ children, value, ...props }, ref) => {
>(({ children, value, className, ...props }, ref) => {
return (
<MenuItem ref={ref} value={value} sx={{ py: 0.75, px: 2 }} {...props}>
<ListItemIcon sx={{ minWidth: 28 }}>
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: 'primary.main' }} />
<MenuItem ref={ref} className={`dropdown-menu-radio-item ${className || ''}`} {...props}>
<ListItemIcon>
<Box className="dropdown-menu-radio-dot" />
</ListItemIcon>
<ListItemText>{children}</ListItemText>
</MenuItem>

View File

@@ -1,8 +1,9 @@
'use client'
import { Box, Popover as MuiPopover, PopoverProps as MuiPopoverProps } from '@mui/material'
import { forwardRef, ReactNode } from 'react'
import { Box, Popover as FakeMuiPopover } from '@/fakemui'
// Popover container
export interface PopoverProps {
children: ReactNode
@@ -28,7 +29,7 @@ const PopoverTrigger = forwardRef<HTMLDivElement, PopoverTriggerProps>(
<Box
ref={ref}
onClick={onClick}
sx={{ display: 'inline-flex', cursor: 'pointer' }}
className="popover-trigger"
{...props}
>
{children}
@@ -39,15 +40,19 @@ const PopoverTrigger = forwardRef<HTMLDivElement, PopoverTriggerProps>(
PopoverTrigger.displayName = 'PopoverTrigger'
// PopoverContent
export interface PopoverContentProps extends Omit<MuiPopoverProps, 'open'> {
export interface PopoverContentProps {
children?: ReactNode
align?: 'start' | 'center' | 'end'
side?: 'top' | 'right' | 'bottom' | 'left'
sideOffset?: number
anchorEl?: HTMLElement | null
onClose?: () => void
className?: string
}
const PopoverContent = forwardRef<HTMLDivElement, PopoverContentProps>(
(
{ children, align = 'center', side = 'bottom', sideOffset = 4, anchorEl, onClose, ...props },
{ children, align = 'center', side = 'bottom', sideOffset = 4, anchorEl, onClose, className, ...props },
ref
) => {
const anchorOrigin = {
@@ -56,7 +61,7 @@ const PopoverContent = forwardRef<HTMLDivElement, PopoverContentProps>(
} as const
return (
<MuiPopover
<FakeMuiPopover
ref={ref}
open={Boolean(anchorEl)}
anchorEl={anchorEl}
@@ -66,18 +71,11 @@ const PopoverContent = forwardRef<HTMLDivElement, PopoverContentProps>(
vertical: side === 'top' ? 'bottom' : 'top',
horizontal: anchorOrigin.horizontal,
}}
PaperProps={{
sx: {
p: 2,
borderRadius: 1,
boxShadow: 3,
mt: sideOffset / 8,
},
}}
className={`popover-content ${className || ''}`}
{...props}
>
{children}
</MuiPopover>
</FakeMuiPopover>
)
}
)
@@ -87,7 +85,7 @@ PopoverContent.displayName = 'PopoverContent'
const PopoverAnchor = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
({ children, ...props }, ref) => {
return (
<Box ref={ref} sx={{ display: 'inline-flex' }} {...props}>
<Box ref={ref} className="popover-anchor" {...props}>
{children}
</Box>
)

View File

@@ -1,5 +1,7 @@
'use client'
import { forwardRef, ReactNode } from 'react'
import {
CheckCircleOutline as CheckCircleOutlineIcon,
Close as CloseIcon,
@@ -7,14 +9,7 @@ import {
InfoOutlined as InfoOutlinedIcon,
WarningAmber as WarningAmberIcon,
} from '@/fakemui/icons'
import {
DialogContent,
DialogContentText,
DialogTitle,
IconButton,
Typography,
} from '@mui/material'
import { forwardRef, ReactNode } from 'react'
import { DialogContent, DialogTitle, IconButton, Typography } from '@/fakemui'
interface AlertDialogContentProps {
children: ReactNode
@@ -35,12 +30,8 @@ const AlertDialogContent = ({
{showCloseButton && onClose && (
<IconButton
onClick={onClose}
sx={{
position: 'absolute',
right: 8,
top: 8,
color: 'text.secondary',
}}
className="alert-dialog-close"
aria-label="Close"
>
<CloseIcon />
</IconButton>
@@ -77,14 +68,7 @@ const AlertDialogHeader = forwardRef<HTMLDivElement, AlertDialogHeaderProps>(
<DialogTitle
ref={ref}
id="alert-dialog-title"
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: iconElement ? 'center' : 'flex-start',
gap: 1,
pt: 3,
pb: 0,
}}
className={`alert-dialog-header ${iconElement ? 'alert-dialog-header--with-icon' : ''}`}
{...props}
>
{iconElement}
@@ -106,12 +90,8 @@ const AlertDialogTitle = forwardRef<HTMLHeadingElement, AlertDialogTitleProps>(
<Typography
ref={ref}
variant="h6"
component="span"
className={className}
sx={{
fontWeight: 600,
textAlign: 'inherit',
}}
as="span"
className={`alert-dialog-title ${className || ''}`}
{...props}
>
{children}
@@ -129,8 +109,10 @@ interface AlertDialogDescriptionProps {
const AlertDialogDescription = forwardRef<HTMLDivElement, AlertDialogDescriptionProps>(
({ children, className, ...props }, ref) => {
return (
<DialogContent ref={ref} className={className} sx={{ pt: 2 }} {...props}>
<DialogContentText id="alert-dialog-description">{children}</DialogContentText>
<DialogContent ref={ref} className={className} {...props}>
<Typography variant="body2" id="alert-dialog-description">
{children}
</Typography>
</DialogContent>
)
}

View File

@@ -1,5 +1,4 @@
import { Card, CardContent } from '@mui/material'
import { Card, CardContent } from '@/fakemui'
import { ArrowRight, Trash } from '@/fakemui/icons'
import {
@@ -141,14 +140,14 @@ export const WorkflowNodeCard = ({
</Badge>
{index < totalNodes - 1 && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<ArrowRightIcon sx={{ fontSize: 12 }} />
<ArrowRight size={12} />
<span>Next</span>
</div>
)}
</div>
</div>
<Button variant="ghost" size="sm" onClick={() => onDeleteNode(node.id)}>
<DeleteIcon sx={{ fontSize: 16 }} />
<Trash size={16} />
</Button>
</div>
</CardContent>