mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-05-06 11:39:36 +00:00
refactor: remove pagination and toast components along with related styles and types
- Deleted TablePagination component and its associated styles and types. - Removed pagination-related exports from index files. - Eliminated sonner toast implementation including its context, container, and configuration. - Cleaned up unused form and table components, including FieldGroup, ValidationSummary, Body, EmptyState, Header, and their respective types.
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import { Level1 } from '@/components/Level1'
|
||||
import type { AppLevel } from '@/lib/level-types'
|
||||
import { getLevelPath } from '@/lib/navigation/get-level-path'
|
||||
|
||||
export function Level1Client() {
|
||||
const router = useRouter()
|
||||
|
||||
const handleNavigate = (level: number) => {
|
||||
const normalizedLevel = Math.min(6, Math.max(1, level)) as AppLevel
|
||||
router.push(getLevelPath(normalizedLevel))
|
||||
}
|
||||
|
||||
return <Level1 onNavigate={handleNavigate} />
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
|
||||
import LevelsClient from './LevelsClient'
|
||||
|
||||
describe('LevelsClient', () => {
|
||||
it('renders permission levels and promotes to the next tier', () => {
|
||||
render(<LevelsClient />)
|
||||
expect(screen.getByText(/Level 1 · Public/)).toBeTruthy()
|
||||
|
||||
const promoteButton = screen.getByRole('button', { name: /Promote to/ })
|
||||
fireEvent.click(promoteButton)
|
||||
|
||||
expect(screen.getByText(/Upgraded to User/)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -1,75 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { Container, Stack, Typography } from '@/fakemui'
|
||||
|
||||
import { LevelDetails } from './components/LevelDetails'
|
||||
import { LevelsGrid } from './components/LevelsGrid'
|
||||
import { PERMISSION_LEVELS } from './levels-data'
|
||||
|
||||
export default function LevelsClient() {
|
||||
const [selectedLevelId, setSelectedLevelId] = useState(PERMISSION_LEVELS[0].id)
|
||||
const [note, setNote] = useState('')
|
||||
|
||||
const selectedLevel = useMemo(
|
||||
() => PERMISSION_LEVELS.find(level => level.id === selectedLevelId) ?? PERMISSION_LEVELS[0],
|
||||
[selectedLevelId]
|
||||
)
|
||||
|
||||
const nextLevel = useMemo(
|
||||
() => PERMISSION_LEVELS.find(level => level.id === selectedLevelId + 1) ?? null,
|
||||
[selectedLevelId]
|
||||
)
|
||||
|
||||
const maxCapabilityCount = useMemo(
|
||||
() => Math.max(...PERMISSION_LEVELS.map(level => level.capabilities.length)),
|
||||
[]
|
||||
)
|
||||
|
||||
const handleSelect = (levelId: number) => {
|
||||
setSelectedLevelId(levelId)
|
||||
setNote(
|
||||
`Selected ${PERMISSION_LEVELS.find(l => l.id === levelId)?.title ?? 'unknown'} privileges.`
|
||||
)
|
||||
}
|
||||
|
||||
const handlePromote = () => {
|
||||
if (!nextLevel) {
|
||||
setNote('You already command the cosmos. No further promotions available.')
|
||||
return
|
||||
}
|
||||
setSelectedLevelId(nextLevel.id)
|
||||
setNote(`Upgraded to ${nextLevel.title}.`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ py: 8 }}>
|
||||
<Stack spacing={4}>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h3" component="h1">
|
||||
The Six Permission Levels
|
||||
</Typography>
|
||||
<Typography color="text.secondary">
|
||||
Level up through Public, User, Moderator, Admin, God, and Super God to unlock the right
|
||||
controls for your role.
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<LevelsGrid
|
||||
levels={PERMISSION_LEVELS}
|
||||
onSelect={handleSelect}
|
||||
selectedLevelId={selectedLevelId}
|
||||
/>
|
||||
|
||||
<LevelDetails
|
||||
selectedLevel={selectedLevel}
|
||||
nextLevel={nextLevel}
|
||||
maxCapabilityCount={maxCapabilityCount}
|
||||
note={note}
|
||||
onPromote={handlePromote}
|
||||
/>
|
||||
</Stack>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Divider,
|
||||
LinearProgress,
|
||||
Paper,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@/fakemui'
|
||||
|
||||
import type { PermissionLevel } from '../levels-data'
|
||||
import { highlightColor } from '../utils/highlightColor'
|
||||
|
||||
type LevelDetailsProps = {
|
||||
selectedLevel: PermissionLevel
|
||||
nextLevel: PermissionLevel | null
|
||||
maxCapabilityCount: number
|
||||
note: string
|
||||
onPromote: () => void
|
||||
}
|
||||
|
||||
export const LevelDetails = ({
|
||||
selectedLevel,
|
||||
nextLevel,
|
||||
maxCapabilityCount,
|
||||
note,
|
||||
onPromote,
|
||||
}: LevelDetailsProps) => (
|
||||
<Paper
|
||||
sx={{
|
||||
p: 4,
|
||||
border: theme => `1px dashed ${theme.palette.divider}`,
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Typography variant="h5">Selected level details</Typography>
|
||||
<Chip label={selectedLevel.badge} size="small" color="secondary" />
|
||||
</Stack>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{selectedLevel.description}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{selectedLevel.capabilities.map(capability => (
|
||||
<Chip
|
||||
key={capability}
|
||||
label={capability}
|
||||
size="small"
|
||||
sx={{ borderColor: highlightColor(selectedLevel) }}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack spacing={1}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={(selectedLevel.capabilities.length / maxCapabilityCount) * 100}
|
||||
sx={{ height: 10, borderRadius: 2 }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{selectedLevel.capabilities.length} of {maxCapabilityCount} capability tiers unlocked
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Next move
|
||||
</Typography>
|
||||
{nextLevel ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Promote into <strong>{nextLevel.title}</strong> to unlock{' '}
|
||||
{nextLevel.capabilities.length} controls.
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Super God reigns supreme. You already own every privilege.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Button variant="contained" onClick={onPromote}>
|
||||
{nextLevel ? `Promote to ${nextLevel.title}` : 'Hold the crown'}
|
||||
</Button>
|
||||
</Box>
|
||||
{note && <Alert severity="info">{note}</Alert>}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Box, Chip, Grid, Paper, Stack, Typography } from '@/fakemui'
|
||||
|
||||
import type { PermissionLevel } from '../levels-data'
|
||||
|
||||
type LevelsGridProps = {
|
||||
levels: PermissionLevel[]
|
||||
selectedLevelId: number
|
||||
onSelect: (levelId: number) => void
|
||||
}
|
||||
|
||||
export const LevelsGrid = ({ levels, selectedLevelId, onSelect }: LevelsGridProps) => (
|
||||
<Grid container spacing={3}>
|
||||
{levels.map(level => (
|
||||
<Grid item xs={12} md={6} lg={4} key={level.id} component="div">
|
||||
<Paper
|
||||
onClick={() => onSelect(level.id)}
|
||||
sx={{
|
||||
border: theme =>
|
||||
`2px solid ${selectedLevelId === level.id ? theme.palette.primary.main : theme.palette.divider}`,
|
||||
p: 3,
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
'&:hover': {
|
||||
borderColor: 'primary.main',
|
||||
},
|
||||
}}
|
||||
elevation={selectedLevelId === level.id ? 6 : 1}
|
||||
>
|
||||
<Box sx={{ position: 'absolute', top: 16, right: 16 }}>
|
||||
<Chip label={level.badge} />
|
||||
</Box>
|
||||
<Typography variant="h6">
|
||||
Level {level.id} · {level.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{level.tagline}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||
{level.description}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{level.capabilities.slice(0, 3).map(capability => (
|
||||
<Chip key={capability} label={capability} size="small" variant="outlined" />
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
@@ -1,100 +0,0 @@
|
||||
export type PermissionLevel = {
|
||||
id: number
|
||||
key: string
|
||||
title: string
|
||||
description: string
|
||||
badge: string
|
||||
capabilities: string[]
|
||||
tagline: string
|
||||
}
|
||||
|
||||
export const PERMISSION_LEVELS: PermissionLevel[] = [
|
||||
{
|
||||
id: 1,
|
||||
key: 'public',
|
||||
title: 'Public',
|
||||
badge: '🌍',
|
||||
description: 'Read-only access to marketing, help, and showcase pages without signing in.',
|
||||
tagline: 'Open browsing with zero authentication.',
|
||||
capabilities: [
|
||||
'Access the landing experience',
|
||||
'Follow feature stories',
|
||||
'Preview public dashboards',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
key: 'user',
|
||||
title: 'User',
|
||||
badge: '🧑💻',
|
||||
description:
|
||||
'Personalized workspace for building content, saving dashboards, and collaborating.',
|
||||
tagline: 'Everyday contributors and team members.',
|
||||
capabilities: [
|
||||
'Edit personal settings',
|
||||
'Manage own content',
|
||||
'Launch saved dashboards',
|
||||
'Join shared workflows',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
key: 'moderator',
|
||||
title: 'Moderator',
|
||||
badge: '🛡️',
|
||||
description:
|
||||
'Protect the community by triaging flags, reviewing reports, and shaping shared spaces.',
|
||||
tagline: 'Guardians of behavior and tone.',
|
||||
capabilities: [
|
||||
'Moderate discussions',
|
||||
'Resolve user flags',
|
||||
'Review incident reports',
|
||||
'Hide or restore content',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
key: 'admin',
|
||||
title: 'Admin',
|
||||
badge: '🧰',
|
||||
description:
|
||||
'Tenant administrators who manage users, billing, policies, and broader content sets.',
|
||||
tagline: 'Operational control for the tenant layer.',
|
||||
capabilities: [
|
||||
'Manage user accounts',
|
||||
'Adjust tenant settings',
|
||||
'Approve packages',
|
||||
'Oversee moderation queue',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
key: 'god',
|
||||
title: 'God',
|
||||
badge: '🧙♂️',
|
||||
description:
|
||||
'Blueprint builders who orchestrate workflows, seed packages, and shape the system architecture.',
|
||||
tagline: 'Power users with advanced scripting rights.',
|
||||
capabilities: [
|
||||
'Author workflows',
|
||||
'Compose the builder UI',
|
||||
'Define multi-tenant templates',
|
||||
'Seed packages',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
key: 'supergod',
|
||||
title: 'Super God',
|
||||
badge: '👑',
|
||||
description:
|
||||
'Full sovereignty over every tenant, infrastructure, and override path in the universe.',
|
||||
tagline: 'Ultimate authority for platform-level change.',
|
||||
capabilities: [
|
||||
'Assign god roles',
|
||||
'Transfer ownership',
|
||||
'Burn and restore tenants',
|
||||
'Run system-wide audits',
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import LevelsClient from './LevelsClient'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Permission Levels',
|
||||
description: 'Explore the five permission tiers that govern MetaBuilder.',
|
||||
}
|
||||
|
||||
export default function LevelsPage() {
|
||||
return <LevelsClient />
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { PermissionLevel } from '../levels-data'
|
||||
|
||||
export const highlightColor = (level: PermissionLevel) => {
|
||||
if (level.id === 6) return 'warning.main'
|
||||
if (level.id === 5) return 'primary.main'
|
||||
return 'divider'
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
# Atoms
|
||||
|
||||
Atoms are the smallest, indivisible UI elements in the MetaBuilder component library. Built on Material UI.
|
||||
|
||||
## Components
|
||||
|
||||
### Controls
|
||||
| Component | Description | MUI Base |
|
||||
|-----------|-------------|----------|
|
||||
| `Button` | Primary action button with variants | `MuiButton` |
|
||||
| `Checkbox` | Boolean toggle with optional label | `MuiCheckbox` |
|
||||
| `Switch` | Toggle switch with optional label | `MuiSwitch` |
|
||||
| `Radio` | Radio button with optional label | `MuiRadio` |
|
||||
|
||||
### Inputs
|
||||
| Component | Description | MUI Base |
|
||||
|-----------|-------------|----------|
|
||||
| `Input` | Text input field | `InputBase` |
|
||||
| `TextArea` | Multi-line text input | `TextareaAutosize` |
|
||||
| `Select` | Dropdown selection | `MuiSelect` |
|
||||
|
||||
### Display
|
||||
| Component | Description | MUI Base |
|
||||
|-----------|-------------|----------|
|
||||
| `Label` | Form field label | `Typography` |
|
||||
| `Badge` | Status indicator chip | `Chip` |
|
||||
| `Avatar` | User/entity image with fallback | `MuiAvatar` |
|
||||
| `IconButton` | Icon-only button | `MuiIconButton` |
|
||||
| `Icon` | Icon wrapper for fakemui icons | `@/fakemui/icons` |
|
||||
| `Link` | Navigation link with Next.js integration | `MuiLink` + `NextLink` |
|
||||
| `Text` | Typography with weight/alignment options | `Typography` |
|
||||
|
||||
### Feedback
|
||||
| Component | Description | MUI Base |
|
||||
|-----------|-------------|----------|
|
||||
| `Skeleton` | Loading placeholder | `MuiSkeleton` |
|
||||
| `Separator` | Visual divider | `Divider` |
|
||||
| `Progress` | Progress indicator | `LinearProgress` |
|
||||
| `Tooltip` | Hover information | `MuiTooltip` |
|
||||
| `Spinner` | Loading spinner | `CircularProgress` |
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Button, Input, TextArea, Select, Radio,
|
||||
Label, Badge, Icon, Link, Text
|
||||
} from '@/components/atoms'
|
||||
|
||||
function MyComponent() {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Label required>Username</Label>
|
||||
<Input placeholder="Enter username" />
|
||||
|
||||
<Label>Bio</Label>
|
||||
<TextArea placeholder="Tell us about yourself" minRows={4} />
|
||||
|
||||
<Label>Country</Label>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'us', label: 'United States' },
|
||||
{ value: 'uk', label: 'United Kingdom' },
|
||||
]}
|
||||
placeholder="Select country"
|
||||
/>
|
||||
|
||||
<Radio label="Subscribe to newsletter" />
|
||||
|
||||
<Text variant="body2" muted>
|
||||
By submitting, you agree to our <Link href="/terms">Terms</Link>
|
||||
</Text>
|
||||
|
||||
<Button variant="contained" startIcon={<Icon name="Save" />}>
|
||||
Save Profile
|
||||
</Button>
|
||||
|
||||
<Badge variant="success" label="Active" />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. ✅ **Single responsibility** - Each atom does one thing well
|
||||
2. ✅ **MUI-powered** - Built on Material UI for consistency
|
||||
3. ✅ **Theme-aware** - Respects light/dark mode via `sx` prop
|
||||
4. ✅ **Accessible** - ARIA attributes and keyboard navigation
|
||||
5. ✅ **Composable** - Combine to build molecules
|
||||
6. ❌ **DON'T** import molecules or organisms
|
||||
7. ❌ **DON'T** add business logic
|
||||
8. ❌ **DON'T** use Tailwind classes (use MUI `sx` prop instead)
|
||||
|
||||
## Styling
|
||||
|
||||
All atoms use the MUI `sx` prop for styling, which provides:
|
||||
- Theme-aware values (colors, spacing, typography)
|
||||
- Responsive breakpoints
|
||||
- Dark/light mode support
|
||||
- Type-safe CSS properties
|
||||
|
||||
```tsx
|
||||
<Button
|
||||
sx={{
|
||||
mt: 2, // theme.spacing(2)
|
||||
bgcolor: 'primary.main',
|
||||
'&:hover': {
|
||||
bgcolor: 'primary.dark'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Styled Button
|
||||
</Button>
|
||||
```
|
||||
@@ -1,81 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Button as FakemuiButton } from '@/fakemui'
|
||||
import type { ButtonProps as FakemuiButtonProps } from '@/fakemui/fakemui/inputs/Button'
|
||||
|
||||
/** Button visual style variants */
|
||||
export type ButtonVariant = 'contained' | 'outlined' | 'text' | 'destructive' | 'ghost'
|
||||
|
||||
/** Button size options */
|
||||
export type ButtonSize = 'small' | 'medium' | 'large' | 'icon'
|
||||
|
||||
/**
|
||||
* Props for the Button component
|
||||
* Wrapper around fakemui Button to maintain API compatibility
|
||||
*/
|
||||
export interface ButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'variant' | 'size'> {
|
||||
/** Visual style variant of the button */
|
||||
variant?: ButtonVariant
|
||||
/** Size of the button */
|
||||
size?: ButtonSize
|
||||
/** Whether to show a loading spinner */
|
||||
loading?: boolean
|
||||
/** Compatibility prop - ignored */
|
||||
asChild?: boolean
|
||||
/** Start icon element */
|
||||
startIcon?: React.ReactNode
|
||||
/** End icon element */
|
||||
endIcon?: React.ReactNode
|
||||
/** Full width button */
|
||||
fullWidth?: boolean
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
}
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ variant = 'contained', size = 'medium', loading, disabled, children, sx, startIcon, endIcon, fullWidth, className, ...props }, ref) => {
|
||||
// Map MUI variants to fakemui variants
|
||||
const fakemuiVariant: FakemuiButtonProps['variant'] =
|
||||
variant === 'contained' ? 'primary' :
|
||||
variant === 'outlined' ? 'outline' :
|
||||
variant === 'text' ? 'text' :
|
||||
variant === 'destructive' ? 'danger' :
|
||||
variant === 'ghost' ? 'ghost' :
|
||||
'default'
|
||||
|
||||
// Map MUI sizes to fakemui sizes
|
||||
const fakemuiSize: FakemuiButtonProps['size'] =
|
||||
size === 'small' ? 'sm' :
|
||||
size === 'large' ? 'lg' :
|
||||
'md'
|
||||
|
||||
const isIcon = size === 'icon'
|
||||
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<FakemuiButton
|
||||
ref={ref}
|
||||
variant={fakemuiVariant}
|
||||
size={fakemuiSize}
|
||||
icon={isIcon}
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
fullWidth={fullWidth}
|
||||
startIcon={startIcon}
|
||||
endIcon={endIcon}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</FakemuiButton>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button }
|
||||
@@ -1,40 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Checkbox as FakemuiCheckbox } from '@/fakemui'
|
||||
|
||||
/**
|
||||
* Props for the Checkbox component
|
||||
* Wrapper around fakemui Checkbox to maintain API compatibility
|
||||
*/
|
||||
export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'> {
|
||||
/** Optional label text to display next to the checkbox */
|
||||
label?: React.ReactNode
|
||||
/** Whether the checkbox is in an error state (MUI compatibility) */
|
||||
error?: boolean
|
||||
/** MUI color prop (ignored for compatibility) */
|
||||
color?: string
|
||||
/** MUI size prop (ignored for compatibility) */
|
||||
size?: string
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
}
|
||||
|
||||
const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(({ label, error, color, size, sx, className, ...props }, ref) => {
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className, error ? 'checkbox--error' : ''].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<FakemuiCheckbox
|
||||
ref={ref}
|
||||
label={label}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
Checkbox.displayName = 'Checkbox'
|
||||
|
||||
export { Checkbox }
|
||||
@@ -1,31 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { Radio } from './Radio'
|
||||
|
||||
describe('Radio', () => {
|
||||
it.each([
|
||||
{ label: 'Option A', value: 'a' },
|
||||
{ label: 'Option B', value: 'b' },
|
||||
{ label: undefined, value: 'c' },
|
||||
])('renders with props %o', ({ label, value }) => {
|
||||
render(<Radio label={label} value={value} />)
|
||||
const radio = screen.getByRole('radio')
|
||||
expect(radio).not.toBeNull()
|
||||
if (label) {
|
||||
expect(screen.getByText(label)).not.toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it('renders checked state', () => {
|
||||
render(<Radio label="Selected" checked onChange={() => {}} />)
|
||||
const radio = screen.getByRole('radio') as HTMLInputElement
|
||||
expect(radio.checked).toBe(true)
|
||||
})
|
||||
|
||||
it('renders disabled state', () => {
|
||||
render(<Radio label="Disabled" disabled />)
|
||||
const radio = screen.getByRole('radio') as HTMLInputElement
|
||||
expect(radio.disabled).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Radio as FakemuiRadio } from '@/fakemui'
|
||||
|
||||
/**
|
||||
* Props for the Radio component
|
||||
* Wrapper around fakemui Radio to maintain API compatibility
|
||||
*/
|
||||
export interface RadioProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'> {
|
||||
/** Optional label text to display next to the radio */
|
||||
label?: React.ReactNode
|
||||
/** MUI color prop (ignored for compatibility) */
|
||||
color?: string
|
||||
/** MUI size prop (ignored for compatibility) */
|
||||
size?: string
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
}
|
||||
|
||||
const Radio = forwardRef<HTMLInputElement, RadioProps>(({ label, color, size, sx, className, ...props }, ref) => {
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<FakemuiRadio
|
||||
ref={ref}
|
||||
label={label}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
Radio.displayName = 'Radio'
|
||||
|
||||
export { Radio }
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Switch as FakemuiSwitch } from '@/fakemui'
|
||||
|
||||
/**
|
||||
* Props for the Switch component
|
||||
* Wrapper around fakemui Switch to maintain API compatibility
|
||||
*/
|
||||
export interface SwitchProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'> {
|
||||
/** Optional label text to display next to the switch */
|
||||
label?: React.ReactNode
|
||||
/** MUI color prop (ignored for compatibility) */
|
||||
color?: string
|
||||
/** MUI size prop (ignored for compatibility) */
|
||||
size?: string
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
}
|
||||
|
||||
const Switch = forwardRef<HTMLInputElement, SwitchProps>(({ label, color, size, sx, className, ...props }, ref) => {
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<FakemuiSwitch
|
||||
ref={ref}
|
||||
label={label}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
Switch.displayName = 'Switch'
|
||||
|
||||
export { Switch }
|
||||
@@ -1,62 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Avatar as FakemuiAvatar, AvatarGroup as FakemuiAvatarGroup } from '@/fakemui'
|
||||
|
||||
/** Avatar size options */
|
||||
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
|
||||
/**
|
||||
* Props for the Avatar component
|
||||
* Wrapper around fakemui Avatar to maintain API compatibility
|
||||
*/
|
||||
export interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Size of the avatar */
|
||||
size?: AvatarSize
|
||||
/** Fallback text to display when no image is provided */
|
||||
fallback?: string
|
||||
/** Image source */
|
||||
src?: string
|
||||
/** Alt text for image */
|
||||
alt?: string
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
}
|
||||
|
||||
const Avatar = forwardRef<HTMLDivElement, AvatarProps>(
|
||||
({ size = 'md', fallback, children, sx, src, alt, className, ...props }, ref) => {
|
||||
// Map size to fakemui size props
|
||||
const sizeProps = {
|
||||
sm: size === 'xs' || size === 'sm',
|
||||
md: size === 'md',
|
||||
lg: size === 'lg',
|
||||
xl: size === 'xl',
|
||||
}
|
||||
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<FakemuiAvatar
|
||||
ref={ref}
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={combinedClassName}
|
||||
{...sizeProps}
|
||||
{...props}
|
||||
>
|
||||
{children || fallback}
|
||||
</FakemuiAvatar>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Avatar.displayName = 'Avatar'
|
||||
|
||||
// Re-export AvatarGroup and create compatibility components
|
||||
const AvatarGroup = FakemuiAvatarGroup
|
||||
const AvatarFallback = ({ children }: { children: React.ReactNode }) => <>{children}</>
|
||||
const AvatarImage = Avatar
|
||||
|
||||
export { Avatar, AvatarFallback, AvatarGroup, AvatarImage }
|
||||
@@ -1,63 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Chip } from '@/fakemui'
|
||||
|
||||
/** Badge visual style variants */
|
||||
export type BadgeVariant =
|
||||
| 'default'
|
||||
| 'secondary'
|
||||
| 'destructive'
|
||||
| 'outline'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
|
||||
/**
|
||||
* Props for the Badge component
|
||||
* Uses fakemui Chip component for badge functionality
|
||||
*/
|
||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Visual style variant of the badge */
|
||||
variant?: BadgeVariant
|
||||
/** Label text */
|
||||
label?: string | React.ReactNode
|
||||
/** Size of the badge */
|
||||
size?: 'small' | 'medium'
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
/** Whether badge is clickable */
|
||||
clickable?: boolean
|
||||
/** Delete handler */
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
const Badge = forwardRef<HTMLDivElement, BadgeProps>(
|
||||
({ variant = 'default', size = 'small', label, children, sx, className, ...props }, ref) => {
|
||||
// Map variant to color class
|
||||
const variantClass =
|
||||
variant === 'destructive' ? 'chip--error' :
|
||||
variant === 'success' ? 'chip--success' :
|
||||
variant === 'warning' ? 'chip--warning' :
|
||||
variant === 'secondary' ? 'chip--secondary' :
|
||||
variant === 'outline' ? 'chip--outline' :
|
||||
''
|
||||
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className, variantClass].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<Chip
|
||||
ref={ref}
|
||||
label={label || children}
|
||||
size={size}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Badge.displayName = 'Badge'
|
||||
|
||||
export { Badge }
|
||||
@@ -1,26 +0,0 @@
|
||||
import { render } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { Icon, type IconName } from './Icon'
|
||||
|
||||
describe('Icon', () => {
|
||||
it.each([
|
||||
{ name: 'Home', size: 'small' },
|
||||
{ name: 'Settings', size: 'medium' },
|
||||
{ name: 'Trash', size: 'large' },
|
||||
{ name: 'Plus', size: 'inherit' },
|
||||
] as const)('renders icon $name with size $size', ({ name, size }) => {
|
||||
const { container } = render(<Icon name={name} size={size} />)
|
||||
expect(container.querySelector('svg')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('applies custom sx styles', () => {
|
||||
const { container } = render(<Icon name="Home" sx={{ color: 'blue' }} />)
|
||||
expect(container.querySelector('svg')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for unknown icon', () => {
|
||||
const { container } = render(<Icon name={'UnknownIcon' as IconName} />)
|
||||
expect(container.querySelector('svg')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,51 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { CSSProperties, forwardRef } from 'react'
|
||||
|
||||
import * as FakeMuiIcons from '@/fakemui/icons'
|
||||
|
||||
// Create a type from the available fakemui icons
|
||||
export type IconName = keyof typeof FakeMuiIcons
|
||||
export type IconSize = 'small' | 'medium' | 'large' | 'inherit'
|
||||
|
||||
export interface IconProps {
|
||||
name: IconName
|
||||
size?: IconSize
|
||||
sx?: CSSProperties & Record<string, unknown>
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
const sizeMap = {
|
||||
small: 20,
|
||||
medium: 24,
|
||||
large: 32,
|
||||
inherit: undefined,
|
||||
}
|
||||
|
||||
const Icon = forwardRef<SVGSVGElement, IconProps>(
|
||||
({ name, size = 'medium', sx, className, style, ...props }, ref) => {
|
||||
const IconComponent = FakeMuiIcons[name]
|
||||
|
||||
if (!IconComponent || typeof IconComponent !== 'function') {
|
||||
console.warn(`Icon "${name}" not found in @/fakemui/icons`)
|
||||
return null
|
||||
}
|
||||
|
||||
const sizeValue = sizeMap[size]
|
||||
const combinedStyle = { ...style, ...sx }
|
||||
|
||||
return (
|
||||
<IconComponent
|
||||
size={sizeValue}
|
||||
style={combinedStyle}
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Icon.displayName = 'Icon'
|
||||
|
||||
export { Icon }
|
||||
@@ -1,57 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { IconButton as FakemuiIconButton } from '@/fakemui'
|
||||
|
||||
/** IconButton size options */
|
||||
export type IconButtonSize = 'small' | 'medium' | 'large'
|
||||
|
||||
/**
|
||||
* Props for the IconButton component
|
||||
* Wrapper around fakemui IconButton to maintain API compatibility
|
||||
*/
|
||||
export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
/** Visual style variant of the icon button */
|
||||
variant?: 'default' | 'outlined' | 'contained'
|
||||
/** Size of the icon button */
|
||||
size?: IconButtonSize
|
||||
/** MUI color prop (ignored for compatibility) */
|
||||
color?: string
|
||||
/** MUI edge prop (ignored for compatibility) */
|
||||
edge?: string | false
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
}
|
||||
|
||||
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
({ variant = 'default', size = 'medium', color, edge, sx, className, ...props }, ref) => {
|
||||
// Map size to fakemui size props
|
||||
const sizeProps = {
|
||||
sm: size === 'small',
|
||||
lg: size === 'large',
|
||||
}
|
||||
|
||||
// Map variant to className
|
||||
const variantClass =
|
||||
variant === 'outlined' ? 'icon-btn--outlined' :
|
||||
variant === 'contained' ? 'icon-btn--contained' :
|
||||
''
|
||||
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className, variantClass].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<FakemuiIconButton
|
||||
ref={ref}
|
||||
className={combinedClassName}
|
||||
{...sizeProps}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
IconButton.displayName = 'IconButton'
|
||||
|
||||
export { IconButton }
|
||||
@@ -1,40 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, LabelHTMLAttributes } from 'react'
|
||||
|
||||
import { Label as FakemuiLabel } from '@/fakemui'
|
||||
|
||||
/**
|
||||
* Props for the Label component
|
||||
* Wrapper around fakemui Label to maintain API compatibility
|
||||
*/
|
||||
export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
|
||||
/** Whether to display a required indicator (*) */
|
||||
required?: boolean
|
||||
/** Whether to style the label as an error state */
|
||||
error?: boolean
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
}
|
||||
|
||||
const Label = forwardRef<HTMLLabelElement, LabelProps>(
|
||||
({ children, required, error, sx, className, ...props }, ref) => {
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [
|
||||
className,
|
||||
sx?.className,
|
||||
error ? 'label--error' : '',
|
||||
].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<label ref={ref} className={`label ${combinedClassName}`} {...props}>
|
||||
{children}
|
||||
{required && <span className="label__required"> *</span>}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Label.displayName = 'Label'
|
||||
|
||||
export { Label }
|
||||
@@ -1,38 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { Link } from './Link'
|
||||
|
||||
describe('Link', () => {
|
||||
it.each([
|
||||
{ href: '/about', external: false, children: 'About Us' },
|
||||
{ href: '/contact', external: false, children: 'Contact' },
|
||||
{ href: 'https://example.com', external: true, children: 'External Link' },
|
||||
])('renders link with props %o', ({ href, external, children }) => {
|
||||
render(
|
||||
<Link href={href} external={external}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
const link = screen.getByText(children)
|
||||
expect(link).not.toBeNull()
|
||||
expect(link.getAttribute('href')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders external link with correct attributes', () => {
|
||||
render(
|
||||
<Link href="https://example.com" external>
|
||||
External
|
||||
</Link>
|
||||
)
|
||||
const link = screen.getByText('External')
|
||||
expect(link.getAttribute('target')).toBe('_blank')
|
||||
expect(link.getAttribute('rel')).toBe('noopener noreferrer')
|
||||
})
|
||||
|
||||
it('renders internal link without target', () => {
|
||||
render(<Link href="/internal">Internal</Link>)
|
||||
const link = screen.getByText('Internal')
|
||||
expect(link.getAttribute('target')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,64 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import NextLink, { LinkProps as NextLinkProps } from 'next/link'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Link as FakemuiLink } from '@/fakemui'
|
||||
|
||||
/**
|
||||
* Props for the Link component
|
||||
* Wrapper around fakemui Link with Next.js integration
|
||||
*/
|
||||
export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
|
||||
/** Link href (Next.js or external URL) */
|
||||
href: NextLinkProps['href']
|
||||
/** Whether this is an external link */
|
||||
external?: boolean
|
||||
/** Link underline style */
|
||||
underline?: 'none' | 'hover' | 'always'
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
/** MUI component prop (ignored for compatibility) */
|
||||
component?: any
|
||||
}
|
||||
|
||||
const Link = forwardRef<HTMLAnchorElement, LinkProps>(
|
||||
({ href, external, children, underline = 'hover', sx, className, component, ...props }, ref) => {
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className].filter(Boolean).join(' ')
|
||||
|
||||
if (external) {
|
||||
return (
|
||||
<FakemuiLink
|
||||
ref={ref}
|
||||
href={href as string}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline={underline}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</FakemuiLink>
|
||||
)
|
||||
}
|
||||
|
||||
// For internal links, wrap fakemui Link with Next.js Link
|
||||
return (
|
||||
<NextLink href={href} passHref legacyBehavior>
|
||||
<FakemuiLink
|
||||
ref={ref}
|
||||
underline={underline}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</FakemuiLink>
|
||||
</NextLink>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Link.displayName = 'Link'
|
||||
|
||||
export { Link }
|
||||
@@ -1,40 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { Text } from './Text'
|
||||
|
||||
describe('Text', () => {
|
||||
it.each([
|
||||
{ variant: 'h1', weight: 'bold', children: 'Heading 1' },
|
||||
{ variant: 'h2', weight: 'semibold', children: 'Heading 2' },
|
||||
{ variant: 'body1', weight: 'regular', children: 'Body text' },
|
||||
{ variant: 'body2', weight: 'light', children: 'Small text' },
|
||||
{ variant: 'caption', weight: 'medium', children: 'Caption' },
|
||||
] as const)('renders with variant=$variant weight=$weight', ({ variant, weight, children }) => {
|
||||
render(
|
||||
<Text variant={variant} weight={weight}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
expect(screen.getByText(children)).not.toBeNull()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ align: 'left', children: 'Left aligned' },
|
||||
{ align: 'center', children: 'Centered' },
|
||||
{ align: 'right', children: 'Right aligned' },
|
||||
] as const)('renders with align=$align', ({ align, children }) => {
|
||||
render(<Text align={align}>{children}</Text>)
|
||||
expect(screen.getByText(children)).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders muted text', () => {
|
||||
render(<Text muted>Muted text</Text>)
|
||||
expect(screen.getByText('Muted text')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders truncated text', () => {
|
||||
render(<Text truncate>Very long text that should truncate</Text>)
|
||||
expect(screen.getByText('Very long text that should truncate')).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,103 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Text as FakemuiText, Typography } from '@/fakemui'
|
||||
|
||||
export type TextVariant =
|
||||
| 'h1'
|
||||
| 'h2'
|
||||
| 'h3'
|
||||
| 'h4'
|
||||
| 'h5'
|
||||
| 'h6'
|
||||
| 'body1'
|
||||
| 'body2'
|
||||
| 'subtitle1'
|
||||
| 'subtitle2'
|
||||
| 'caption'
|
||||
| 'overline'
|
||||
|
||||
export type TextWeight = 'light' | 'regular' | 'medium' | 'semibold' | 'bold'
|
||||
export type TextAlign = 'left' | 'center' | 'right' | 'justify'
|
||||
|
||||
/**
|
||||
* Props for the Text component
|
||||
* Wrapper around fakemui Text/Typography to maintain API compatibility
|
||||
*/
|
||||
export interface TextProps extends React.HTMLAttributes<HTMLElement> {
|
||||
/** Typography variant (MUI compatibility) */
|
||||
variant?: TextVariant
|
||||
/** Font weight */
|
||||
weight?: TextWeight
|
||||
/** Text alignment */
|
||||
align?: TextAlign
|
||||
/** Muted/secondary text style */
|
||||
muted?: boolean
|
||||
/** Truncate text with ellipsis */
|
||||
truncate?: boolean
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
/** MUI component prop - specify HTML element */
|
||||
component?: React.ElementType
|
||||
}
|
||||
|
||||
const weightMap = {
|
||||
light: 'font-light',
|
||||
regular: 'font-normal',
|
||||
medium: 'font-medium',
|
||||
semibold: 'font-semibold',
|
||||
bold: 'font-bold',
|
||||
}
|
||||
|
||||
const Text = forwardRef<HTMLElement, TextProps>(
|
||||
(
|
||||
{ variant = 'body1', weight = 'regular', align = 'left', muted, truncate, sx, className, component, ...props },
|
||||
ref
|
||||
) => {
|
||||
// For heading variants, use Typography
|
||||
if (variant && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(variant)) {
|
||||
const combinedClassName = [
|
||||
className,
|
||||
sx?.className,
|
||||
weightMap[weight],
|
||||
muted ? 'text-secondary' : '',
|
||||
truncate ? 'truncate' : '',
|
||||
`text-${align}`,
|
||||
].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<Typography
|
||||
ref={ref as any}
|
||||
variant={variant}
|
||||
className={combinedClassName}
|
||||
as={component}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// For body/caption variants, use fakemui Text
|
||||
const combinedClassName = [
|
||||
className,
|
||||
sx?.className,
|
||||
weightMap[weight],
|
||||
`text-${align}`,
|
||||
].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<FakemuiText
|
||||
ref={ref as any}
|
||||
secondary={muted}
|
||||
truncate={truncate}
|
||||
className={combinedClassName}
|
||||
as={component || 'span'}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Text.displayName = 'Text'
|
||||
|
||||
export { Text }
|
||||
@@ -1,79 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { Component, ReactNode } from 'react'
|
||||
|
||||
import { Alert, Button, Typography } from '@/fakemui'
|
||||
import { logError, LogLevel } from '@/lib/errors/log-error'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
fallback?: ReactNode
|
||||
onError?: (error: Error, errorInfo: React.ErrorInfo) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error?: Error
|
||||
}
|
||||
|
||||
/**
|
||||
* Error Boundary component to catch React rendering errors
|
||||
* Logs errors and displays a fallback UI
|
||||
*/
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
// Log error with component stack
|
||||
logError(
|
||||
error,
|
||||
{
|
||||
component: 'ErrorBoundary',
|
||||
componentStack: errorInfo.componentStack,
|
||||
},
|
||||
LogLevel.ERROR
|
||||
)
|
||||
|
||||
// Call custom error handler if provided
|
||||
this.props.onError?.(error, errorInfo)
|
||||
}
|
||||
|
||||
handleReset = (): void => {
|
||||
this.setState({ hasError: false, error: undefined })
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.hasError) {
|
||||
// Use custom fallback if provided
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback
|
||||
}
|
||||
|
||||
// Default error UI
|
||||
return (
|
||||
<div style={{ padding: '1.5rem' }}>
|
||||
<Alert severity="error" style={{ marginBottom: '1rem' }}>
|
||||
<Typography variant="h6">
|
||||
Something went wrong
|
||||
</Typography>
|
||||
<Typography variant="body2" style={{ marginBottom: '1rem' }}>
|
||||
{this.state.error?.message || 'An unexpected error occurred'}
|
||||
</Typography>
|
||||
<Button variant="primary" onClick={this.handleReset} size="sm">
|
||||
Try Again
|
||||
</Button>
|
||||
</Alert>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { CircularProgress,LinearProgress } from '@/fakemui'
|
||||
|
||||
/**
|
||||
* Props for the Progress component
|
||||
* Wrapper around fakemui LinearProgress to maintain API compatibility
|
||||
*/
|
||||
export interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Progress value (0-100) */
|
||||
value?: number
|
||||
/** Whether to display a percentage label next to the progress bar */
|
||||
showLabel?: boolean
|
||||
/** Variant of the progress bar */
|
||||
variant?: 'determinate' | 'indeterminate'
|
||||
/** Color of the progress bar */
|
||||
color?: string
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
}
|
||||
|
||||
const Progress = forwardRef<HTMLDivElement, ProgressProps>(
|
||||
({ value, showLabel, variant, color, sx, className, ...props }, ref) => {
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className].filter(Boolean).join(' ')
|
||||
|
||||
if (showLabel && value !== undefined) {
|
||||
return (
|
||||
<div ref={ref} className={`progress-with-label ${combinedClassName}`} style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<LinearProgress
|
||||
value={value}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
<span className="progress-label text-secondary" style={{ minWidth: '40px' }}>
|
||||
{Math.round(value)}%
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<LinearProgress
|
||||
ref={ref}
|
||||
value={value}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Progress.displayName = 'Progress'
|
||||
|
||||
// Also export CircularProgress for convenience
|
||||
export { CircularProgress, Progress }
|
||||
@@ -1,39 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Divider } from '@/fakemui'
|
||||
|
||||
/**
|
||||
* Props for the Separator component
|
||||
* Wrapper around fakemui Divider to maintain API compatibility
|
||||
*/
|
||||
export interface SeparatorProps extends React.HTMLAttributes<HTMLHRElement> {
|
||||
/** Orientation of the separator */
|
||||
orientation?: 'horizontal' | 'vertical'
|
||||
/** Whether the separator is decorative (for accessibility) */
|
||||
decorative?: boolean
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
}
|
||||
|
||||
const Separator = forwardRef<HTMLHRElement, SeparatorProps>(
|
||||
({ orientation = 'horizontal', decorative, sx, className, ...props }, ref) => {
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<Divider
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
role={decorative ? 'presentation' : 'separator'}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Separator.displayName = 'Separator'
|
||||
|
||||
export { Separator }
|
||||
@@ -1,52 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Skeleton as FakemuiSkeleton } from '@/fakemui'
|
||||
import type { SkeletonProps as FakemuiSkeletonProps } from '@/fakemui/fakemui/feedback/Skeleton'
|
||||
|
||||
/**
|
||||
* Props for the Skeleton component
|
||||
* Wrapper around fakemui Skeleton to maintain API compatibility
|
||||
*/
|
||||
export interface SkeletonProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
/** Shape variant */
|
||||
variant?: 'text' | 'rectangular' | 'circular' | 'rounded'
|
||||
/** Animation type */
|
||||
animation?: 'pulse' | 'wave' | false
|
||||
/** Width of the skeleton */
|
||||
width?: string | number
|
||||
/** Height of the skeleton */
|
||||
height?: string | number
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
}
|
||||
|
||||
const Skeleton = forwardRef<HTMLSpanElement, SkeletonProps>(
|
||||
({ variant = 'rounded', animation = 'wave', width, height, sx, className, ...props }, ref) => {
|
||||
// Map MUI variant to fakemui variant
|
||||
const fakemuiVariant = variant === 'rounded' ? 'rectangular' : variant
|
||||
|
||||
// Map MUI animation to fakemui animation
|
||||
const fakemuiAnimation = animation === 'wave' ? 'pulse' : animation
|
||||
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<FakemuiSkeleton
|
||||
ref={ref}
|
||||
variant={fakemuiVariant}
|
||||
animation={fakemuiAnimation}
|
||||
width={width}
|
||||
height={height}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Skeleton.displayName = 'Skeleton'
|
||||
|
||||
export { Skeleton }
|
||||
@@ -1,62 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { CircularProgress } from '@/fakemui'
|
||||
|
||||
/** Spinner size options */
|
||||
export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg'
|
||||
|
||||
/**
|
||||
* Props for the Spinner component
|
||||
* Wrapper around fakemui CircularProgress to maintain API compatibility
|
||||
*/
|
||||
export interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Size of the spinner (xs: 16px, sm: 20px, md: 24px, lg: 40px) or a custom number */
|
||||
size?: SpinnerSize | number
|
||||
/** Whether to center the spinner in its container */
|
||||
centered?: boolean
|
||||
/** Color of the spinner */
|
||||
color?: string
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
}
|
||||
|
||||
const sizeMap: Record<SpinnerSize, number> = {
|
||||
xs: 16,
|
||||
sm: 20,
|
||||
md: 24,
|
||||
lg: 40,
|
||||
}
|
||||
|
||||
const Spinner = forwardRef<HTMLDivElement, SpinnerProps>(
|
||||
({ size = 'md', centered, color, sx, className, ...props }, ref) => {
|
||||
const dimension = typeof size === 'number' ? size : sizeMap[size]
|
||||
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className].filter(Boolean).join(' ')
|
||||
|
||||
const spinner = (
|
||||
<CircularProgress
|
||||
ref={ref}
|
||||
size={dimension}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
if (centered) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '1rem' }}>
|
||||
{spinner}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return spinner
|
||||
}
|
||||
)
|
||||
|
||||
Spinner.displayName = 'Spinner'
|
||||
|
||||
export { Spinner }
|
||||
@@ -1,75 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactElement, ReactNode } from 'react'
|
||||
|
||||
import { Tooltip as FakemuiTooltip } from '@/fakemui'
|
||||
|
||||
/**
|
||||
* Props for the Tooltip component
|
||||
* Wrapper around fakemui Tooltip to maintain API compatibility
|
||||
*/
|
||||
export interface TooltipProps {
|
||||
/** The element that triggers the tooltip */
|
||||
children: ReactElement
|
||||
/** Title or main content of the tooltip */
|
||||
title?: ReactNode
|
||||
/** Alias for title - main content of the tooltip */
|
||||
content?: ReactNode
|
||||
/** Position of the tooltip relative to its trigger */
|
||||
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||
/** Delay in milliseconds before showing the tooltip */
|
||||
delayDuration?: number
|
||||
/** Whether to display an arrow pointing to the trigger element */
|
||||
arrow?: boolean
|
||||
/** Controlled open state */
|
||||
open?: boolean
|
||||
/** Callback when tooltip is opened */
|
||||
onOpen?: () => void
|
||||
/** Callback when tooltip is closed */
|
||||
onClose?: () => void
|
||||
/** MUI placement prop (mapped to side) */
|
||||
placement?: 'top' | 'right' | 'bottom' | 'left'
|
||||
/** MUI enterDelay prop (mapped to delayDuration) */
|
||||
enterDelay?: number
|
||||
}
|
||||
|
||||
const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
content,
|
||||
title,
|
||||
side,
|
||||
placement,
|
||||
delayDuration,
|
||||
enterDelay,
|
||||
arrow = true,
|
||||
open,
|
||||
onOpen,
|
||||
onClose,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<FakemuiTooltip
|
||||
title={content || title || ''}
|
||||
placement={side || placement || 'top'}
|
||||
arrow={arrow}
|
||||
open={open}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</FakemuiTooltip>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Tooltip.displayName = 'Tooltip'
|
||||
|
||||
// Compatibility exports
|
||||
const TooltipTrigger = ({ children }: { children: ReactElement }) => children
|
||||
const TooltipContent = ({ children }: { children: React.ReactNode }) => <>{children}</>
|
||||
const TooltipProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
||||
@@ -1,48 +0,0 @@
|
||||
// Atoms - Basic building blocks
|
||||
// These are the smallest, indivisible UI components that form the foundation
|
||||
|
||||
// Controls
|
||||
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant } from './controls/Button'
|
||||
export { Checkbox, type CheckboxProps } from './controls/Checkbox'
|
||||
export { Radio, type RadioProps } from './controls/Radio'
|
||||
export { Switch, type SwitchProps } from './controls/Switch'
|
||||
|
||||
// Inputs
|
||||
export { Input, type InputProps } from './inputs/Input'
|
||||
export { Select, type SelectOption, type SelectProps } from './inputs/Select'
|
||||
export { TextArea, type TextAreaProps } from './inputs/TextArea'
|
||||
|
||||
// Display
|
||||
export {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarImage,
|
||||
type AvatarProps,
|
||||
type AvatarSize,
|
||||
} from './display/Avatar'
|
||||
export { Badge, type BadgeProps, type BadgeVariant } from './display/Badge'
|
||||
export { Icon, type IconName, type IconProps, type IconSize } from './display/Icon'
|
||||
export { IconButton, type IconButtonProps, type IconButtonSize } from './display/IconButton'
|
||||
export { Label, type LabelProps } from './display/Label'
|
||||
export { Link, type LinkProps } from './display/Link'
|
||||
export {
|
||||
Text,
|
||||
type TextAlign,
|
||||
type TextProps,
|
||||
type TextVariant,
|
||||
type TextWeight,
|
||||
} from './display/Text'
|
||||
|
||||
// Feedback
|
||||
export { CircularProgress, Progress, type ProgressProps } from './feedback/Progress'
|
||||
export { Separator, type SeparatorProps } from './feedback/Separator'
|
||||
export { Skeleton, type SkeletonProps } from './feedback/Skeleton'
|
||||
export { Spinner, type SpinnerProps, type SpinnerSize } from './feedback/Spinner'
|
||||
export {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
type TooltipProps,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from './feedback/Tooltip'
|
||||
@@ -1,49 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Input as FakemuiInput } from '@/fakemui'
|
||||
import type { InputProps as FakemuiInputProps } from '@/fakemui/fakemui/inputs/Input'
|
||||
|
||||
/**
|
||||
* Props for the Input component
|
||||
* Wrapper around fakemui Input to maintain API compatibility
|
||||
*/
|
||||
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||
/** Whether the input is in an error state */
|
||||
error?: boolean
|
||||
/** Whether the input should take up the full width of its container */
|
||||
fullWidth?: boolean
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
/** MUI inputRef - forwarded as ref */
|
||||
inputRef?: React.Ref<HTMLInputElement>
|
||||
/** Start adornment element */
|
||||
startAdornment?: React.ReactNode
|
||||
/** End adornment element */
|
||||
endAdornment?: React.ReactNode
|
||||
}
|
||||
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ type, error, fullWidth = true, sx, inputRef, startAdornment, endAdornment, className, ...props }, ref) => {
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<FakemuiInput
|
||||
ref={inputRef || ref}
|
||||
type={type}
|
||||
error={error}
|
||||
fullWidth={fullWidth}
|
||||
startAdornment={startAdornment}
|
||||
endAdornment={endAdornment}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export { Input }
|
||||
@@ -1,39 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { Select } from './Select'
|
||||
|
||||
const mockOptions = [
|
||||
{ value: 'opt1', label: 'Option 1' },
|
||||
{ value: 'opt2', label: 'Option 2' },
|
||||
{ value: 'opt3', label: 'Option 3', disabled: true },
|
||||
]
|
||||
|
||||
describe('Select', () => {
|
||||
it.each([
|
||||
{ options: mockOptions, placeholder: 'Choose option', fullWidth: true },
|
||||
{ options: mockOptions, placeholder: undefined, fullWidth: false },
|
||||
])('renders with props %o', props => {
|
||||
render(<Select {...props} value="" onChange={() => {}} />)
|
||||
const select = screen.getByRole('combobox')
|
||||
expect(select).not.toBeNull()
|
||||
})
|
||||
|
||||
it('shows placeholder when provided', () => {
|
||||
render(<Select options={mockOptions} placeholder="Select..." value="" onChange={() => {}} />)
|
||||
const select = screen.getByRole('combobox')
|
||||
expect(select).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders all options', () => {
|
||||
render(<Select options={mockOptions} value="" onChange={() => {}} />)
|
||||
const select = screen.getByRole('combobox')
|
||||
expect(select).not.toBeNull()
|
||||
})
|
||||
|
||||
it('shows error state', () => {
|
||||
render(<Select options={mockOptions} error value="" onChange={() => {}} />)
|
||||
const select = screen.getByRole('combobox')
|
||||
expect(select).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Select as FakemuiSelect } from '@/fakemui'
|
||||
|
||||
export interface SelectOption {
|
||||
value: string | number
|
||||
label: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the Select component
|
||||
* Wrapper around fakemui Select to maintain API compatibility
|
||||
*/
|
||||
export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'children'> {
|
||||
/** Array of select options */
|
||||
options: SelectOption[]
|
||||
/** Placeholder text */
|
||||
placeholder?: string
|
||||
/** Error state */
|
||||
error?: boolean
|
||||
/** Full width */
|
||||
fullWidth?: boolean
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
/** MUI displayEmpty prop (ignored for compatibility) */
|
||||
displayEmpty?: boolean
|
||||
}
|
||||
|
||||
const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||
({ options, error, fullWidth = true, placeholder, sx, className, displayEmpty, ...props }, ref) => {
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className, error ? 'select--error' : ''].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<FakemuiSelect
|
||||
ref={ref}
|
||||
fullWidth={fullWidth}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
>
|
||||
{placeholder && (
|
||||
<option value="" disabled>
|
||||
{placeholder}
|
||||
</option>
|
||||
)}
|
||||
{options.map(option => (
|
||||
<option key={option.value} value={option.value} disabled={option.disabled}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</FakemuiSelect>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Select.displayName = 'Select'
|
||||
|
||||
export { Select }
|
||||
@@ -1,29 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { TextArea } from './TextArea'
|
||||
|
||||
describe('TextArea', () => {
|
||||
it.each([
|
||||
{ placeholder: 'Enter text', fullWidth: true },
|
||||
{ placeholder: 'Comment here', fullWidth: false },
|
||||
{ placeholder: 'Description', minRows: 5 },
|
||||
])('renders with props %o', props => {
|
||||
render(<TextArea {...props} />)
|
||||
const textarea = screen.getByPlaceholderText(props.placeholder)
|
||||
expect(textarea).not.toBeNull()
|
||||
})
|
||||
|
||||
it('shows error state', () => {
|
||||
render(<TextArea error placeholder="Enter text" />)
|
||||
const textarea = screen.getByPlaceholderText('Enter text')
|
||||
expect(textarea).not.toBeNull()
|
||||
})
|
||||
|
||||
it('accepts placeholder prop', () => {
|
||||
const placeholder = 'Type here'
|
||||
render(<TextArea placeholder={placeholder} />)
|
||||
const textarea = screen.getByPlaceholderText(placeholder) as HTMLTextAreaElement
|
||||
expect(textarea.placeholder).toBe(placeholder)
|
||||
})
|
||||
})
|
||||
@@ -1,44 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Textarea } from '@/fakemui'
|
||||
|
||||
/**
|
||||
* Props for the TextArea component
|
||||
* Wrapper around fakemui Textarea to maintain API compatibility
|
||||
*/
|
||||
export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
/** Whether the textarea is in an error state */
|
||||
error?: boolean
|
||||
/** Whether the textarea should take up the full width of its container */
|
||||
fullWidth?: boolean
|
||||
/** Minimum number of rows */
|
||||
minRows?: number
|
||||
/** Maximum number of rows */
|
||||
maxRows?: number
|
||||
/** MUI sx prop - converted to className for compatibility */
|
||||
sx?: any
|
||||
}
|
||||
|
||||
const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
({ error, fullWidth = true, minRows = 3, maxRows, sx, className, ...props }, ref) => {
|
||||
// Combine className with any sx-based classes
|
||||
const combinedClassName = [className, sx?.className, error ? 'textarea--error' : ''].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
ref={ref}
|
||||
rows={minRows}
|
||||
error={error}
|
||||
fullWidth={fullWidth}
|
||||
className={combinedClassName}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
TextArea.displayName = 'TextArea'
|
||||
|
||||
export { TextArea }
|
||||
@@ -1,112 +0,0 @@
|
||||
# Molecules
|
||||
|
||||
Molecules are simple groups of atoms that function together as a cohesive unit. Built on Material UI.
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Description | Atoms Used |
|
||||
|-----------|-------------|------------|
|
||||
| `Card` | Container with header/content/footer | Box, Typography |
|
||||
| `Dialog` | Modal dialog with transitions | MuiDialog |
|
||||
| `Alert` | Feedback message with severity | MuiAlert |
|
||||
| `Tabs` | Tab navigation | MuiTabs, MuiTab |
|
||||
| `Accordion` | Collapsible sections | MuiAccordion |
|
||||
| `Select` | Dropdown selection | MuiSelect |
|
||||
| `DropdownMenu` | Context/action menu | Menu, MenuItem |
|
||||
| `FormField` | Label + input + error | Label, Input |
|
||||
| `SearchInput` | Input with search icon | TextField |
|
||||
| `PasswordField` | Password input with visibility toggle | TextField, IconButton |
|
||||
| `EmailField` | Email input with icon | TextField, InputAdornment |
|
||||
| `NumberField` | Number input with constraints | TextField |
|
||||
| `SearchBar` | Search input with clear and filter buttons | TextField, IconButton |
|
||||
| `Popover` | Floating content panel | MuiPopover |
|
||||
|
||||
### Application Molecules
|
||||
- `AppHeader` - Application header with logo and navigation
|
||||
- `AppFooter` - Application footer with links
|
||||
- `ProfileCard` - User profile display card
|
||||
- `SecurityWarningDialog` - Security warning modal
|
||||
- `PasswordChangeDialog` - Password change form modal
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Card, CardHeader, CardContent,
|
||||
Dialog, Alert,
|
||||
PasswordField, EmailField, NumberField, SearchBar
|
||||
} from '@/components/molecules'
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
<Box>
|
||||
<Card>
|
||||
<CardHeader title="Title" description="Subtitle" />
|
||||
<CardContent>Content here</CardContent>
|
||||
</Card>
|
||||
|
||||
<Alert variant="success" title="Success!">
|
||||
Operation completed.
|
||||
</Alert>
|
||||
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent>Modal content</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<PasswordField
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<EmailField
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
showIcon
|
||||
/>
|
||||
|
||||
<NumberField
|
||||
label="Age"
|
||||
min={0}
|
||||
max={120}
|
||||
value={age}
|
||||
onChange={(e) => setAge(e.target.value)}
|
||||
/>
|
||||
|
||||
<SearchBar
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
onClear={() => setSearchQuery('')}
|
||||
showFilterButton
|
||||
onFilterClick={handleOpenFilters}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. ✅ **DO** compose molecules from atoms
|
||||
2. ✅ **DO** keep molecules focused on a single purpose
|
||||
3. ✅ **DO** use MUI `sx` prop for styling
|
||||
4. ✅ **DO** support both controlled and uncontrolled modes
|
||||
5. ❌ **DON'T** import organisms
|
||||
6. ❌ **DON'T** add complex business logic
|
||||
7. ❌ **DON'T** use Tailwind classes (use MUI `sx` prop)
|
||||
|
||||
## Styling
|
||||
|
||||
```tsx
|
||||
<Card sx={{ maxWidth: 400, mx: 'auto' }}>
|
||||
<CardHeader
|
||||
title="User Profile"
|
||||
sx={{ bgcolor: 'primary.light' }}
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography>Content with theme colors</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import {
|
||||
Accordion as FakeMuiAccordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Typography,
|
||||
} from '@/fakemui'
|
||||
import { ExpandMore } from '@/fakemui/icons'
|
||||
|
||||
// Accordion (single item)
|
||||
export interface AccordionProps {
|
||||
children: ReactNode
|
||||
expanded?: boolean
|
||||
disabled?: boolean
|
||||
type?: 'single' | 'multiple'
|
||||
collapsible?: boolean
|
||||
value?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Accordion = forwardRef<HTMLDivElement, AccordionProps>(
|
||||
({ children, type = 'single', collapsible, expanded, disabled, className, ...props }, ref) => {
|
||||
return (
|
||||
<FakeMuiAccordion
|
||||
ref={ref}
|
||||
expanded={expanded}
|
||||
disabled={disabled}
|
||||
className={`accordion ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</FakeMuiAccordion>
|
||||
)
|
||||
}
|
||||
)
|
||||
Accordion.displayName = 'Accordion'
|
||||
|
||||
// AccordionItem (for shadcn compat - wraps Accordion)
|
||||
interface AccordionItemProps {
|
||||
children: ReactNode
|
||||
value: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
|
||||
({ children, value, className, ...props }, ref) => {
|
||||
return (
|
||||
<Accordion ref={ref} className={className} {...props}>
|
||||
{children}
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
)
|
||||
AccordionItem.displayName = 'AccordionItem'
|
||||
|
||||
// AccordionTrigger
|
||||
interface AccordionTriggerProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<AccordionSummary
|
||||
ref={ref}
|
||||
expandIcon={<ExpandMore />}
|
||||
className={`accordion-trigger ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
<Typography variant="body1" className="font-medium">
|
||||
{children}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
)
|
||||
}
|
||||
)
|
||||
AccordionTrigger.displayName = 'AccordionTrigger'
|
||||
|
||||
// AccordionContent
|
||||
interface AccordionContentProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<AccordionDetails ref={ref} className={`accordion-content ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</AccordionDetails>
|
||||
)
|
||||
}
|
||||
)
|
||||
AccordionContent.displayName = 'AccordionContent'
|
||||
|
||||
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Alert as FakeMuiAlert, AlertTitle as FakeMuiAlertTitle, IconButton } from '@/fakemui'
|
||||
import { Close } from '@/fakemui/icons'
|
||||
|
||||
export type AlertVariant = 'default' | 'destructive' | 'success' | 'warning' | 'info'
|
||||
|
||||
export interface AlertProps {
|
||||
variant?: AlertVariant
|
||||
title?: ReactNode
|
||||
dismissible?: boolean
|
||||
onDismiss?: () => void
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const variantMap: Record<AlertVariant, 'info' | 'error' | 'success' | 'warning'> = {
|
||||
default: 'info',
|
||||
destructive: 'error',
|
||||
success: 'success',
|
||||
warning: 'warning',
|
||||
info: 'info',
|
||||
}
|
||||
|
||||
const Alert = forwardRef<HTMLDivElement, AlertProps>(
|
||||
(
|
||||
{ variant = 'default', title, dismissible, onDismiss, children, className, ...props },
|
||||
ref
|
||||
) => {
|
||||
const severity = variantMap[variant]
|
||||
|
||||
return (
|
||||
<FakeMuiAlert
|
||||
ref={ref}
|
||||
severity={severity}
|
||||
variant="outlined"
|
||||
className={`alert ${className || ''}`}
|
||||
action={
|
||||
dismissible && onDismiss ? (
|
||||
<IconButton aria-label="close" onClick={onDismiss}>
|
||||
<Close size={16} />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{title && <FakeMuiAlertTitle>{title}</FakeMuiAlertTitle>}
|
||||
{children}
|
||||
</FakeMuiAlert>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Alert.displayName = 'Alert'
|
||||
|
||||
// AlertTitle (for direct use)
|
||||
const AlertTitle = FakeMuiAlertTitle
|
||||
|
||||
// AlertDescription (shadcn compat)
|
||||
const AlertDescription = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<div ref={ref} className={`alert-description ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
AlertDescription.displayName = 'AlertDescription'
|
||||
|
||||
export { Alert, AlertDescription, AlertTitle }
|
||||
@@ -1,117 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import {
|
||||
Box,
|
||||
Card as FakeMuiCard,
|
||||
CardActions,
|
||||
CardContent as FakeMuiCardContent,
|
||||
CardHeader as FakeMuiCardHeader,
|
||||
Typography,
|
||||
} from '@/fakemui'
|
||||
|
||||
// Card
|
||||
export interface CardProps {
|
||||
children?: ReactNode
|
||||
noPadding?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Card = forwardRef<HTMLDivElement, CardProps>(({ noPadding, className, ...props }, ref) => {
|
||||
return (
|
||||
<FakeMuiCard
|
||||
ref={ref}
|
||||
className={`card ${noPadding ? 'card--no-padding' : ''} ${className || ''}`}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Card.displayName = 'Card'
|
||||
|
||||
// CardHeader
|
||||
export interface CardHeaderProps {
|
||||
title?: ReactNode
|
||||
subheader?: ReactNode
|
||||
action?: ReactNode
|
||||
avatar?: ReactNode
|
||||
description?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
|
||||
({ description, subheader, ...props }, ref) => {
|
||||
return (
|
||||
<FakeMuiCardHeader
|
||||
ref={ref}
|
||||
subheader={description || subheader}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
CardHeader.displayName = 'CardHeader'
|
||||
|
||||
// CardTitle (for shadcn compatibility)
|
||||
interface CardTitleProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Typography ref={ref} variant="h6" className={`card-title ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</Typography>
|
||||
)
|
||||
})
|
||||
CardTitle.displayName = 'CardTitle'
|
||||
|
||||
// CardDescription
|
||||
interface CardDescriptionProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CardDescription = forwardRef<HTMLParagraphElement, CardDescriptionProps>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Typography ref={ref} variant="body2" className={`card-description ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
)
|
||||
CardDescription.displayName = 'CardDescription'
|
||||
|
||||
// CardContent
|
||||
const CardContent = forwardRef<HTMLDivElement, { children?: ReactNode; className?: string }>(
|
||||
({ className, ...props }, ref) => {
|
||||
return <FakeMuiCardContent ref={ref} className={`card-content ${className || ''}`} {...props} />
|
||||
}
|
||||
)
|
||||
CardContent.displayName = 'CardContent'
|
||||
|
||||
// CardFooter / CardActions
|
||||
const CardFooter = forwardRef<HTMLDivElement, { children?: ReactNode; className?: string }>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<CardActions ref={ref} className={`card-footer ${className || ''}`} {...props} />
|
||||
)
|
||||
}
|
||||
)
|
||||
CardFooter.displayName = 'CardFooter'
|
||||
|
||||
// CardAction (for header actions - shadcn compat)
|
||||
const CardAction = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`card-action ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
CardAction.displayName = 'CardAction'
|
||||
|
||||
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
||||
@@ -1,66 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { EmailField } from './EmailField'
|
||||
|
||||
describe('EmailField', () => {
|
||||
it.each([
|
||||
{ label: 'Email', placeholder: 'you@example.com', showIcon: true },
|
||||
{ label: 'Your Email', placeholder: 'Enter email', showIcon: false },
|
||||
{ label: 'Work Email', placeholder: undefined, showIcon: true },
|
||||
])(
|
||||
'renders with label "$label", placeholder "$placeholder", showIcon $showIcon',
|
||||
({ label, placeholder, showIcon }) => {
|
||||
render(<EmailField label={label} placeholder={placeholder} showIcon={showIcon} />)
|
||||
|
||||
expect(screen.getByLabelText(label)).toBeTruthy()
|
||||
if (placeholder) {
|
||||
expect(screen.getByPlaceholderText(placeholder)).toBeTruthy()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('renders with email icon by default', () => {
|
||||
const { container } = render(<EmailField />)
|
||||
|
||||
// Icon is rendered via MUI Icon component
|
||||
expect(container.querySelector('svg')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('does not render icon when showIcon is false', () => {
|
||||
const { container } = render(<EmailField showIcon={false} />)
|
||||
|
||||
// No icon should be present
|
||||
expect(container.querySelector('svg')).toBeNull()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ error: 'Invalid email', helperText: undefined },
|
||||
{ error: undefined, helperText: 'Enter a valid email address' },
|
||||
{ error: 'Required field', helperText: 'Please provide your email' },
|
||||
])('displays error "$error" or helperText "$helperText"', ({ error, helperText }) => {
|
||||
render(<EmailField error={error} helperText={helperText} />)
|
||||
|
||||
const displayText = error || helperText
|
||||
if (displayText) {
|
||||
expect(screen.getByText(displayText)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it('calls onChange when value changes', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(<EmailField onChange={handleChange} />)
|
||||
|
||||
const input = screen.getByLabelText('Email')
|
||||
fireEvent.change(input, { target: { value: 'test@example.com' } })
|
||||
|
||||
expect(handleChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('has type="email" attribute', () => {
|
||||
render(<EmailField />)
|
||||
|
||||
const input = screen.getByLabelText('Email') as HTMLInputElement
|
||||
expect(input.type).toBe('email')
|
||||
})
|
||||
})
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { TextField } from '@/fakemui'
|
||||
import { Email } from '@/fakemui/icons'
|
||||
|
||||
export interface EmailFieldProps {
|
||||
label?: string
|
||||
name?: string
|
||||
value?: string
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
error?: string
|
||||
helperText?: string
|
||||
required?: boolean
|
||||
placeholder?: string
|
||||
fullWidth?: boolean
|
||||
disabled?: boolean
|
||||
autoComplete?: string
|
||||
showIcon?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const EmailField = forwardRef<HTMLInputElement, EmailFieldProps>(
|
||||
(
|
||||
{
|
||||
label = 'Email',
|
||||
name = 'email',
|
||||
value,
|
||||
onChange,
|
||||
error,
|
||||
helperText,
|
||||
required = false,
|
||||
placeholder = 'you@example.com',
|
||||
fullWidth = true,
|
||||
disabled = false,
|
||||
autoComplete = 'email',
|
||||
showIcon = true,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<TextField
|
||||
ref={ref}
|
||||
type="email"
|
||||
label={label}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={!!error}
|
||||
helperText={error || helperText}
|
||||
required={required}
|
||||
placeholder={placeholder}
|
||||
fullWidth={fullWidth}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
className={className}
|
||||
startAdornment={
|
||||
showIcon ? (
|
||||
<Email size={16} style={{ color: 'rgba(0,0,0,0.54)' }} />
|
||||
) : undefined
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
EmailField.displayName = 'EmailField'
|
||||
|
||||
export { EmailField }
|
||||
@@ -1,64 +0,0 @@
|
||||
.formField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.helperText {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary, #666);
|
||||
|
||||
&.error {
|
||||
color: var(--error-main, #d32f2f);
|
||||
}
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.searchIcon {
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: auto;
|
||||
min-height: 80px;
|
||||
padding: 8px 12px;
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
border: 1px solid var(--divider, rgba(0, 0, 0, 0.12));
|
||||
border-radius: 4px;
|
||||
background-color: var(--background-paper, #fff);
|
||||
resize: vertical;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-main, #1976d2);
|
||||
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.15);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: var(--error-main, #d32f2f);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--error-main, #d32f2f);
|
||||
box-shadow: 0 0 0 2px rgba(211, 47, 47, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
&.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box, TextField } from '@/fakemui'
|
||||
import { Search } from '@/fakemui/icons'
|
||||
|
||||
import { Label } from '../atoms/Label'
|
||||
import styles from './FormField.module.scss'
|
||||
|
||||
// FormField - combines label and input
|
||||
export interface FormFieldProps {
|
||||
label?: string
|
||||
name?: string
|
||||
error?: string
|
||||
required?: boolean
|
||||
helperText?: string
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const FormField = forwardRef<HTMLDivElement, FormFieldProps>(
|
||||
({ label, name, error, required, helperText, children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`${styles.formField} ${className || ''}`} {...props}>
|
||||
{label && (
|
||||
<Label htmlFor={name} required={required} error={!!error}>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
{children}
|
||||
{(error || helperText) && (
|
||||
<span className={`${styles.helperText} ${error ? styles.error : ''}`}>
|
||||
{error || helperText}
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
FormField.displayName = 'FormField'
|
||||
|
||||
// SearchInput - input with search icon
|
||||
export interface SearchInputProps {
|
||||
value?: string
|
||||
onChange?: (value: string) => void
|
||||
placeholder?: string
|
||||
fullWidth?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
||||
({ value, onChange, placeholder = 'Search...', fullWidth = true, className, ...props }, ref) => {
|
||||
return (
|
||||
<TextField
|
||||
ref={ref}
|
||||
value={value}
|
||||
onChange={e => onChange?.(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
fullWidth={fullWidth}
|
||||
className={`${styles.searchInput} ${className || ''}`}
|
||||
startAdornment={<Search size={16} className={styles.searchIcon} />}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
SearchInput.displayName = 'SearchInput'
|
||||
|
||||
// TextArea
|
||||
export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
error?: boolean
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
({ error, fullWidth = true, className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
className={`${styles.textarea} ${error ? styles.error : ''} ${fullWidth ? styles.fullWidth : ''} ${className || ''}`}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
TextArea.displayName = 'TextArea'
|
||||
|
||||
export { FormField, SearchInput, TextArea }
|
||||
@@ -1,12 +0,0 @@
|
||||
.numberField {
|
||||
// Hide number input spinners
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { NumberField } from './NumberField'
|
||||
|
||||
describe('NumberField', () => {
|
||||
it.each([
|
||||
{ label: 'Number', value: undefined },
|
||||
{ label: 'Age', value: 25 },
|
||||
{ label: 'Quantity', value: 100 },
|
||||
])('renders with label "$label" and value $value', ({ label, value }) => {
|
||||
render(<NumberField label={label} value={value} />)
|
||||
|
||||
expect(screen.getByLabelText(label)).toBeTruthy()
|
||||
if (value !== undefined) {
|
||||
expect(screen.getByDisplayValue(value.toString())).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ min: 0, max: 100, step: 1 },
|
||||
{ min: -10, max: 10, step: 0.5 },
|
||||
{ min: undefined, max: undefined, step: undefined },
|
||||
])('respects min $min, max $max, step $step constraints', ({ min, max, step }) => {
|
||||
render(<NumberField min={min} max={max} step={step} />)
|
||||
|
||||
const input = screen.getByLabelText('Number') as HTMLInputElement
|
||||
|
||||
if (min !== undefined) {
|
||||
expect(input.min).toBe(min.toString())
|
||||
}
|
||||
if (max !== undefined) {
|
||||
expect(input.max).toBe(max.toString())
|
||||
}
|
||||
if (step !== undefined) {
|
||||
expect(input.step).toBe(step.toString())
|
||||
} else {
|
||||
expect(input.step).toBe('1')
|
||||
}
|
||||
})
|
||||
|
||||
it('calls onChange when value changes', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(<NumberField onChange={handleChange} />)
|
||||
|
||||
const input = screen.getByLabelText('Number')
|
||||
fireEvent.change(input, { target: { value: '42' } })
|
||||
|
||||
expect(handleChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ error: 'Value too high', helperText: undefined },
|
||||
{ error: undefined, helperText: 'Enter a number between 0 and 100' },
|
||||
])('displays error "$error" or helperText "$helperText"', ({ error, helperText }) => {
|
||||
render(<NumberField error={error} helperText={helperText} />)
|
||||
|
||||
const displayText = error || helperText
|
||||
if (displayText) {
|
||||
expect(screen.getByText(displayText)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it('has type="number" attribute', () => {
|
||||
render(<NumberField />)
|
||||
|
||||
const input = screen.getByLabelText('Number') as HTMLInputElement
|
||||
expect(input.type).toBe('number')
|
||||
})
|
||||
})
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { TextField } from '@/fakemui'
|
||||
|
||||
import styles from './NumberField.module.scss'
|
||||
|
||||
export interface NumberFieldProps {
|
||||
label?: string
|
||||
name?: string
|
||||
value?: number | string
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
error?: string
|
||||
helperText?: string
|
||||
required?: boolean
|
||||
placeholder?: string
|
||||
fullWidth?: boolean
|
||||
disabled?: boolean
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number | string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
|
||||
(
|
||||
{
|
||||
label = 'Number',
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
error,
|
||||
helperText,
|
||||
required = false,
|
||||
placeholder,
|
||||
fullWidth = true,
|
||||
disabled = false,
|
||||
min,
|
||||
max,
|
||||
step = 1,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<TextField
|
||||
ref={ref}
|
||||
type="number"
|
||||
label={label}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={!!error}
|
||||
helperText={error || helperText}
|
||||
required={required}
|
||||
placeholder={placeholder}
|
||||
fullWidth={fullWidth}
|
||||
disabled={disabled}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
className={`${styles.numberField} ${className || ''}`}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
NumberField.displayName = 'NumberField'
|
||||
|
||||
export { NumberField }
|
||||
@@ -1,64 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { PasswordField } from './PasswordField'
|
||||
|
||||
describe('PasswordField', () => {
|
||||
it.each([
|
||||
{ label: 'Password', placeholder: undefined },
|
||||
{ label: 'Enter Password', placeholder: 'Your password' },
|
||||
{ label: 'Confirm Password', placeholder: 'Confirm your password' },
|
||||
])('renders with label "$label" and placeholder "$placeholder"', ({ label, placeholder }) => {
|
||||
render(<PasswordField label={label} placeholder={placeholder} />)
|
||||
|
||||
expect(screen.getByLabelText(label)).toBeTruthy()
|
||||
if (placeholder) {
|
||||
expect(screen.getByPlaceholderText(placeholder)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it('toggles password visibility when icon button is clicked', () => {
|
||||
render(<PasswordField />)
|
||||
|
||||
const input = screen.getByLabelText('Password') as HTMLInputElement
|
||||
expect(input.type).toBe('password')
|
||||
|
||||
const toggleButton = screen.getByLabelText('toggle password visibility')
|
||||
fireEvent.click(toggleButton)
|
||||
|
||||
expect(input.type).toBe('text')
|
||||
|
||||
fireEvent.click(toggleButton)
|
||||
expect(input.type).toBe('password')
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ error: 'Password is required', helperText: undefined },
|
||||
{ error: undefined, helperText: 'Must be at least 8 characters' },
|
||||
{ error: 'Too short', helperText: 'Should be longer' },
|
||||
])('displays error "$error" or helperText "$helperText"', ({ error, helperText }) => {
|
||||
render(<PasswordField error={error} helperText={helperText} />)
|
||||
|
||||
const displayText = error || helperText
|
||||
if (displayText) {
|
||||
expect(screen.getByText(displayText)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it('calls onChange when value changes', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(<PasswordField onChange={handleChange} />)
|
||||
|
||||
const input = screen.getByLabelText('Password')
|
||||
fireEvent.change(input, { target: { value: 'newpassword' } })
|
||||
|
||||
expect(handleChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('disables toggle button when field is disabled', () => {
|
||||
render(<PasswordField disabled />)
|
||||
|
||||
const toggleButton = screen.getByLabelText('toggle password visibility')
|
||||
expect(toggleButton.hasAttribute('disabled')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,87 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, useState } from 'react'
|
||||
|
||||
import { IconButton, TextField } from '@/fakemui'
|
||||
import { Visibility, VisibilityOff } from '@/fakemui/icons'
|
||||
|
||||
export interface PasswordFieldProps {
|
||||
label?: string
|
||||
name?: string
|
||||
value?: string
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
error?: string
|
||||
helperText?: string
|
||||
required?: boolean
|
||||
placeholder?: string
|
||||
fullWidth?: boolean
|
||||
disabled?: boolean
|
||||
autoComplete?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
|
||||
(
|
||||
{
|
||||
label = 'Password',
|
||||
name = 'password',
|
||||
value,
|
||||
onChange,
|
||||
error,
|
||||
helperText,
|
||||
required = false,
|
||||
placeholder,
|
||||
fullWidth = true,
|
||||
disabled = false,
|
||||
autoComplete = 'current-password',
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
const togglePasswordVisibility = () => {
|
||||
setShowPassword(prev => !prev)
|
||||
}
|
||||
|
||||
return (
|
||||
<TextField
|
||||
ref={ref}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
label={label}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={!!error}
|
||||
helperText={error || helperText}
|
||||
required={required}
|
||||
placeholder={placeholder}
|
||||
fullWidth={fullWidth}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
className={className}
|
||||
endAdornment={
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={togglePasswordVisibility}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
disabled={disabled}
|
||||
sm
|
||||
>
|
||||
{showPassword ? (
|
||||
<VisibilityOff size={16} />
|
||||
) : (
|
||||
<Visibility size={16} />
|
||||
)}
|
||||
</IconButton>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
PasswordField.displayName = 'PasswordField'
|
||||
|
||||
export { PasswordField }
|
||||
@@ -1,28 +0,0 @@
|
||||
.searchBar {
|
||||
border-radius: 8px;
|
||||
background-color: var(--background-paper, #fff);
|
||||
transition: box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.searchIcon {
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
|
||||
.endAdornments {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.clearButton,
|
||||
.filterButton {
|
||||
padding: 4px;
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { SearchBar } from './SearchBar'
|
||||
|
||||
describe('SearchBar', () => {
|
||||
it.each([
|
||||
{ placeholder: 'Search...', value: '' },
|
||||
{ placeholder: 'Find items...', value: 'test query' },
|
||||
{ placeholder: 'Type to search', value: 'example' },
|
||||
])('renders with placeholder "$placeholder" and value "$value"', ({ placeholder, value }) => {
|
||||
render(<SearchBar placeholder={placeholder} value={value} />)
|
||||
|
||||
expect(screen.getByPlaceholderText(placeholder)).toBeTruthy()
|
||||
if (value) {
|
||||
expect(screen.getByDisplayValue(value)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it('shows search icon by default', () => {
|
||||
const { container } = render(<SearchBar />)
|
||||
|
||||
// Search icon is always present
|
||||
expect(container.querySelector('svg')).toBeTruthy()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ showClearButton: true, value: 'test', shouldShowClear: true },
|
||||
{ showClearButton: false, value: 'test', shouldShowClear: false },
|
||||
{ showClearButton: true, value: '', shouldShowClear: false },
|
||||
])(
|
||||
'handles clear button with showClearButton=$showClearButton, value="$value"',
|
||||
({ showClearButton, value, shouldShowClear }) => {
|
||||
render(<SearchBar showClearButton={showClearButton} value={value} />)
|
||||
|
||||
const clearButton = screen.queryByLabelText('clear search')
|
||||
if (shouldShowClear) {
|
||||
expect(clearButton).toBeTruthy()
|
||||
} else {
|
||||
expect(clearButton).toBeNull()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('calls onClear when clear button is clicked', () => {
|
||||
const handleClear = vi.fn()
|
||||
const handleChange = vi.fn()
|
||||
render(<SearchBar value="test" onClear={handleClear} onChange={handleChange} />)
|
||||
|
||||
const clearButton = screen.getByLabelText('clear search')
|
||||
fireEvent.click(clearButton)
|
||||
|
||||
expect(handleClear).toHaveBeenCalled()
|
||||
expect(handleChange).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it.each([{ showFilterButton: true }, { showFilterButton: false }])(
|
||||
'renders filter button when showFilterButton=$showFilterButton',
|
||||
({ showFilterButton }) => {
|
||||
render(<SearchBar showFilterButton={showFilterButton} />)
|
||||
|
||||
const filterButton = screen.queryByLabelText('open filters')
|
||||
if (showFilterButton) {
|
||||
expect(filterButton).toBeTruthy()
|
||||
} else {
|
||||
expect(filterButton).toBeNull()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('calls onFilterClick when filter button is clicked', () => {
|
||||
const handleFilterClick = vi.fn()
|
||||
render(<SearchBar showFilterButton onFilterClick={handleFilterClick} />)
|
||||
|
||||
const filterButton = screen.getByLabelText('open filters')
|
||||
fireEvent.click(filterButton)
|
||||
|
||||
expect(handleFilterClick).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('calls onChange when input value changes', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(<SearchBar onChange={handleChange} />)
|
||||
|
||||
const input = screen.getByPlaceholderText('Search...')
|
||||
fireEvent.change(input, { target: { value: 'new search' } })
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith('new search')
|
||||
})
|
||||
})
|
||||
@@ -1,99 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box, IconButton, TextField } from '@/fakemui'
|
||||
import { Clear, FilterList, Search } from '@/fakemui/icons'
|
||||
|
||||
import styles from './SearchBar.module.scss'
|
||||
|
||||
export interface SearchBarProps {
|
||||
value?: string
|
||||
onChange?: (value: string) => void
|
||||
onClear?: () => void
|
||||
onFilterClick?: () => void
|
||||
placeholder?: string
|
||||
fullWidth?: boolean
|
||||
showFilterButton?: boolean
|
||||
showClearButton?: boolean
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
endAdornment?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SearchBar = forwardRef<HTMLInputElement, SearchBarProps>(
|
||||
(
|
||||
{
|
||||
value = '',
|
||||
onChange,
|
||||
onClear,
|
||||
onFilterClick,
|
||||
placeholder = 'Search...',
|
||||
fullWidth = true,
|
||||
showFilterButton = false,
|
||||
showClearButton = true,
|
||||
disabled = false,
|
||||
loading = false,
|
||||
endAdornment,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange?.(e.target.value)
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
onChange?.('')
|
||||
onClear?.()
|
||||
}
|
||||
|
||||
return (
|
||||
<TextField
|
||||
ref={ref}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
fullWidth={fullWidth}
|
||||
disabled={disabled}
|
||||
className={`${styles.searchBar} ${className || ''}`}
|
||||
startAdornment={
|
||||
<Search size={16} className={styles.searchIcon} />
|
||||
}
|
||||
endAdornment={
|
||||
<Box className={styles.endAdornments}>
|
||||
{showClearButton && value && !disabled && (
|
||||
<IconButton
|
||||
aria-label="clear search"
|
||||
onClick={handleClear}
|
||||
sm
|
||||
className={styles.clearButton}
|
||||
>
|
||||
<Clear size={16} />
|
||||
</IconButton>
|
||||
)}
|
||||
{showFilterButton && (
|
||||
<IconButton
|
||||
aria-label="open filters"
|
||||
onClick={onFilterClick}
|
||||
disabled={disabled}
|
||||
sm
|
||||
className={styles.filterButton}
|
||||
>
|
||||
<FilterList size={16} />
|
||||
</IconButton>
|
||||
)}
|
||||
{endAdornment}
|
||||
</Box>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SearchBar.displayName = 'SearchBar'
|
||||
|
||||
export { SearchBar }
|
||||
@@ -1,28 +0,0 @@
|
||||
.selectWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.selectContainer {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
pointer-events: none;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode, SelectHTMLAttributes } from 'react'
|
||||
|
||||
import { Box, FormControl, FormHelperText, FormLabel, Select as FakeMuiSelect } from '@/fakemui'
|
||||
import { KeyboardArrowDown } from '@/fakemui/icons'
|
||||
|
||||
import styles from './Select.module.scss'
|
||||
import { SelectContent } from './SelectContent'
|
||||
import { SelectGroup } from './SelectGroup'
|
||||
import type { SelectItemProps } from './SelectItem'
|
||||
import { SelectItem } from './SelectItem'
|
||||
import { SelectLabel } from './SelectLabel'
|
||||
import { SelectSeparator } from './SelectSeparator'
|
||||
import { SelectTrigger } from './SelectTrigger'
|
||||
import { SelectValue } from './SelectValue'
|
||||
|
||||
export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'onChange'> {
|
||||
onValueChange?: (value: string) => void
|
||||
helperText?: ReactNode
|
||||
label?: ReactNode
|
||||
error?: boolean
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||
(
|
||||
{
|
||||
onValueChange,
|
||||
value,
|
||||
defaultValue,
|
||||
label,
|
||||
error,
|
||||
helperText,
|
||||
children,
|
||||
className,
|
||||
fullWidth = true,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<FormControl className={`${styles.selectWrapper} ${fullWidth ? styles.fullWidth : ''} ${className || ''}`}>
|
||||
{label && <FormLabel>{label}</FormLabel>}
|
||||
<Box className={styles.selectContainer}>
|
||||
<FakeMuiSelect
|
||||
ref={ref}
|
||||
value={value as string}
|
||||
defaultValue={defaultValue as string}
|
||||
onChange={e => onValueChange?.(e.target.value)}
|
||||
error={error}
|
||||
className={styles.select}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</FakeMuiSelect>
|
||||
<KeyboardArrowDown size={16} className={styles.icon} />
|
||||
</Box>
|
||||
{helperText && <FormHelperText error={error}>{helperText}</FormHelperText>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
)
|
||||
Select.displayName = 'Select'
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
export type { SelectItemProps }
|
||||
@@ -1,22 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
interface SelectContentProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SelectContent = forwardRef<HTMLDivElement, SelectContentProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<div ref={ref} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SelectContent.displayName = 'SelectContent'
|
||||
|
||||
export { SelectContent }
|
||||
@@ -1,22 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box } from '@/fakemui'
|
||||
|
||||
interface SelectGroupProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SelectGroup = forwardRef<HTMLDivElement, SelectGroupProps>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
SelectGroup.displayName = 'SelectGroup'
|
||||
|
||||
export { SelectGroup }
|
||||
@@ -1,26 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { ButtonHTMLAttributes,forwardRef } from 'react'
|
||||
|
||||
import { MenuItem } from '@/fakemui'
|
||||
|
||||
export interface SelectItemProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
value?: string | number
|
||||
textValue?: string
|
||||
selected?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const SelectItem = forwardRef<HTMLButtonElement, SelectItemProps>(
|
||||
({ value, children, selected, disabled, ...props }, ref) => {
|
||||
return (
|
||||
<MenuItem ref={ref} selected={selected} disabled={disabled} data-value={value} {...props}>
|
||||
{children}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SelectItem.displayName = 'SelectItem'
|
||||
|
||||
export { SelectItem }
|
||||
@@ -1,6 +0,0 @@
|
||||
.selectLabel {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box } from '@/fakemui'
|
||||
|
||||
import styles from './SelectLabel.module.scss'
|
||||
|
||||
interface SelectLabelProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SelectLabel = forwardRef<HTMLDivElement, SelectLabelProps>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`${styles.selectLabel} ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SelectLabel.displayName = 'SelectLabel'
|
||||
|
||||
export { SelectLabel }
|
||||
@@ -1,4 +0,0 @@
|
||||
.selectSeparator {
|
||||
margin: 4px 0;
|
||||
border-color: var(--divider, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { Divider } from '@/fakemui'
|
||||
|
||||
import styles from './SelectSeparator.module.scss'
|
||||
|
||||
interface SelectSeparatorProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SelectSeparator = forwardRef<HTMLHRElement, SelectSeparatorProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return <Divider ref={ref} className={`${styles.selectSeparator} ${className || ''}`} {...props} />
|
||||
}
|
||||
)
|
||||
|
||||
SelectSeparator.displayName = 'SelectSeparator'
|
||||
|
||||
export { SelectSeparator }
|
||||
@@ -1,19 +0,0 @@
|
||||
.selectTrigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--divider, rgba(0, 0, 0, 0.12));
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--text-secondary, #666);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 4px;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box } from '@/fakemui'
|
||||
import { KeyboardArrowDown } from '@/fakemui/icons'
|
||||
|
||||
import styles from './SelectTrigger.module.scss'
|
||||
|
||||
interface SelectTriggerProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SelectTrigger = forwardRef<HTMLDivElement, SelectTriggerProps>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`${styles.selectTrigger} ${className || ''}`} {...props}>
|
||||
{children}
|
||||
<KeyboardArrowDown size={16} className={styles.icon} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SelectTrigger.displayName = 'SelectTrigger'
|
||||
|
||||
export { SelectTrigger }
|
||||
@@ -1,9 +0,0 @@
|
||||
.selectValue {
|
||||
&.hasValue {
|
||||
color: var(--text-primary, rgba(0, 0, 0, 0.87));
|
||||
}
|
||||
|
||||
&.placeholder {
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box } from '@/fakemui'
|
||||
|
||||
import styles from './SelectValue.module.scss'
|
||||
|
||||
interface SelectValueProps {
|
||||
placeholder?: string
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SelectValue = forwardRef<HTMLSpanElement, SelectValueProps>(
|
||||
({ placeholder, children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
component="span"
|
||||
ref={ref}
|
||||
className={`${styles.selectValue} ${children ? styles.hasValue : styles.placeholder} ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{children || placeholder}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SelectValue.displayName = 'SelectValue'
|
||||
|
||||
export { SelectValue }
|
||||
@@ -1,30 +0,0 @@
|
||||
.tabsWrapper {
|
||||
// Container for tabs
|
||||
}
|
||||
|
||||
.tabs {
|
||||
// Tabs container styling
|
||||
}
|
||||
|
||||
.tabsList {
|
||||
display: inline-flex;
|
||||
background-color: var(--action-hover, rgba(0, 0, 0, 0.04));
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.tabTrigger {
|
||||
min-height: 36px;
|
||||
padding: 8px 24px;
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
border-radius: 2px;
|
||||
|
||||
&.tab--active {
|
||||
background-color: var(--background-paper, #fff);
|
||||
}
|
||||
}
|
||||
|
||||
.tabContent {
|
||||
padding-top: 16px;
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { ButtonHTMLAttributes, forwardRef, HTMLAttributes } from 'react'
|
||||
|
||||
import { Box, Tab as FakeMuiTab, TabProps as FakeMuiTabProps,Tabs as FakeMuiTabs } from '@/fakemui'
|
||||
|
||||
import styles from './Tabs.module.scss'
|
||||
|
||||
// Tabs container
|
||||
export interface TabsProps extends HTMLAttributes<HTMLDivElement> {
|
||||
defaultValue?: string
|
||||
value?: string
|
||||
onValueChange?: (value: string) => void
|
||||
variant?: 'standard' | 'scrollable' | 'fullWidth'
|
||||
}
|
||||
|
||||
const Tabs = forwardRef<HTMLDivElement, TabsProps>(
|
||||
({ defaultValue, value, onValueChange, children, className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`${styles.tabsWrapper} ${className || ''}`} {...props}>
|
||||
<FakeMuiTabs
|
||||
value={value || defaultValue}
|
||||
onChange={(_, newValue) => onValueChange?.(newValue)}
|
||||
variant={variant}
|
||||
className={styles.tabs}
|
||||
>
|
||||
{children}
|
||||
</FakeMuiTabs>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
Tabs.displayName = 'Tabs'
|
||||
|
||||
// TabsList (wrapper for tabs - for shadcn compat)
|
||||
const TabsList = forwardRef<HTMLDivElement, { children: React.ReactNode; className?: string }>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`${styles.tabsList} ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
TabsList.displayName = 'TabsList'
|
||||
|
||||
// TabsTrigger (individual tab)
|
||||
export interface TabsTriggerProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'value'> {
|
||||
value: string
|
||||
label?: React.ReactNode
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
const TabsTrigger = forwardRef<HTMLButtonElement, TabsTriggerProps>(
|
||||
({ value, label, children, className, selected, ...props }, ref) => {
|
||||
return (
|
||||
<FakeMuiTab
|
||||
ref={ref}
|
||||
value={value}
|
||||
label={label || children}
|
||||
selected={selected}
|
||||
className={`${styles.tabTrigger} ${className || ''}`}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
TabsTrigger.displayName = 'TabsTrigger'
|
||||
|
||||
// TabsContent (content panel)
|
||||
interface TabsContentProps {
|
||||
value: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
forceMount?: boolean
|
||||
}
|
||||
|
||||
const TabsContent = forwardRef<HTMLDivElement, TabsContentProps>(
|
||||
({ value, children, forceMount, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
role="tabpanel"
|
||||
hidden={!forceMount}
|
||||
id={`tabpanel-${value}`}
|
||||
aria-labelledby={`tab-${value}`}
|
||||
className={`${styles.tabContent} ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
TabsContent.displayName = 'TabsContent'
|
||||
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger }
|
||||
@@ -1,109 +0,0 @@
|
||||
// Molecules - Simple combinations of atoms
|
||||
// These combine atoms to create more complex, reusable components
|
||||
|
||||
// New MUI-based molecules
|
||||
export {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
type AccordionProps,
|
||||
AccordionTrigger,
|
||||
} from './display/Accordion'
|
||||
export {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
type AlertProps,
|
||||
AlertTitle,
|
||||
type AlertVariant,
|
||||
} from './display/Alert'
|
||||
export {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
type CardHeaderProps,
|
||||
type CardProps,
|
||||
CardTitle,
|
||||
} from './display/Card'
|
||||
export { EmailField, type EmailFieldProps } from './form/EmailField'
|
||||
export {
|
||||
FormField,
|
||||
type FormFieldProps,
|
||||
SearchInput,
|
||||
type SearchInputProps,
|
||||
TextArea,
|
||||
type TextAreaProps,
|
||||
} from './form/FormField'
|
||||
export { NumberField, type NumberFieldProps } from './form/NumberField'
|
||||
export { PasswordField, type PasswordFieldProps } from './form/PasswordField'
|
||||
export { SearchBar, type SearchBarProps } from './form/SearchBar'
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
type SelectItemProps,
|
||||
SelectLabel,
|
||||
type SelectProps,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from './form/Select'
|
||||
export {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
type TabsProps,
|
||||
TabsTrigger,
|
||||
type TabsTriggerProps,
|
||||
} from './form/Tabs'
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
type DialogProps,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from './overlay/Dialog'
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
type DropdownMenuContentProps,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
type DropdownMenuItemProps,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
type DropdownMenuProps,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from './overlay/DropdownMenu'
|
||||
export {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
type PopoverContentProps,
|
||||
type PopoverProps,
|
||||
PopoverTrigger,
|
||||
} from './overlay/Popover'
|
||||
|
||||
// Legacy shared components
|
||||
export { GodCredentialsBanner } from '../level1/GodCredentialsBanner'
|
||||
export { ProfileCard } from '../level2/ProfileCard'
|
||||
export { PasswordChangeDialog } from '../PasswordChangeDialog'
|
||||
export { AppFooter } from '../shared/AppFooter'
|
||||
export { AppHeader } from '../shared/AppHeader'
|
||||
@@ -1,187 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import {
|
||||
Box,
|
||||
DialogActions,
|
||||
DialogContent as FakeMuiDialogContent,
|
||||
DialogTitle as FakeMuiDialogTitle,
|
||||
IconButton,
|
||||
Modal,
|
||||
Slide,
|
||||
Typography,
|
||||
} from '@/fakemui'
|
||||
import { Close } from '@/fakemui/icons'
|
||||
|
||||
// Dialog
|
||||
export interface DialogProps {
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
onClose?: () => void
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Dialog = forwardRef<HTMLDivElement, DialogProps>(
|
||||
({ open, onOpenChange, onClose, children, className, ...props }, ref) => {
|
||||
const handleClose = () => {
|
||||
onClose?.()
|
||||
onOpenChange?.(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={handleClose} className={className} {...props}>
|
||||
<Slide direction="up" in={open}>
|
||||
<div ref={ref} className="dialog-panel">{children}</div>
|
||||
</Slide>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
)
|
||||
Dialog.displayName = 'Dialog'
|
||||
|
||||
// DialogTrigger
|
||||
interface DialogTriggerProps {
|
||||
children: ReactNode
|
||||
asChild?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const DialogTrigger = forwardRef<HTMLDivElement, DialogTriggerProps>(
|
||||
({ children, onClick, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
className="dialog-trigger"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
DialogTrigger.displayName = 'DialogTrigger'
|
||||
|
||||
// DialogContent
|
||||
interface DialogContentProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
onClose?: () => void
|
||||
showCloseButton?: boolean
|
||||
}
|
||||
|
||||
const DialogContent = forwardRef<HTMLDivElement, DialogContentProps>(
|
||||
({ children, showCloseButton = true, onClose, className, ...props }, ref) => {
|
||||
return (
|
||||
<FakeMuiDialogContent
|
||||
ref={ref}
|
||||
className={`dialog-content ${showCloseButton ? 'dialog-content--with-close' : ''} ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{showCloseButton && onClose && (
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={onClose}
|
||||
className="dialog-close-button"
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
)}
|
||||
{children}
|
||||
</FakeMuiDialogContent>
|
||||
)
|
||||
}
|
||||
)
|
||||
DialogContent.displayName = 'DialogContent'
|
||||
|
||||
// DialogHeader
|
||||
const DialogHeader = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`dialog-header ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
DialogHeader.displayName = 'DialogHeader'
|
||||
|
||||
// DialogFooter
|
||||
const DialogFooter = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<DialogActions ref={ref} className={`dialog-footer ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</DialogActions>
|
||||
)
|
||||
}
|
||||
)
|
||||
DialogFooter.displayName = 'DialogFooter'
|
||||
|
||||
// DialogTitle
|
||||
const DialogTitle = forwardRef<HTMLHeadingElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<FakeMuiDialogTitle ref={ref} className={`dialog-title ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</FakeMuiDialogTitle>
|
||||
)
|
||||
}
|
||||
)
|
||||
DialogTitle.displayName = 'DialogTitle'
|
||||
|
||||
// DialogDescription
|
||||
const DialogDescription = forwardRef<
|
||||
HTMLParagraphElement,
|
||||
{ children: ReactNode; className?: string }
|
||||
>(({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Typography variant="body2" className={`dialog-description ${className || ''}`} {...props}>
|
||||
<span ref={ref}>{children}</span>
|
||||
</Typography>
|
||||
)
|
||||
})
|
||||
DialogDescription.displayName = 'DialogDescription'
|
||||
|
||||
// DialogClose
|
||||
const DialogClose = forwardRef<
|
||||
HTMLButtonElement,
|
||||
{ children?: ReactNode; onClick?: () => void; asChild?: boolean }
|
||||
>(({ children, onClick, ...props }, ref) => {
|
||||
if (children) {
|
||||
return (
|
||||
<Box
|
||||
onClick={onClick}
|
||||
className="dialog-close"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<IconButton ref={ref} aria-label="close" onClick={onClick} {...props}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
)
|
||||
})
|
||||
DialogClose.displayName = 'DialogClose'
|
||||
|
||||
// Compatibility exports
|
||||
const DialogPortal = ({ children }: { children: ReactNode }) => <>{children}</>
|
||||
const DialogOverlay = forwardRef<HTMLDivElement>((props, ref) => <div ref={ref} {...props} />)
|
||||
DialogOverlay.displayName = 'DialogOverlay'
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
'use client'
|
||||
|
||||
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 FakeMUI Menu under the hood)
|
||||
export interface DropdownMenuProps {
|
||||
children: ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
const DropdownMenu = ({ children, open, onOpenChange }: DropdownMenuProps) => {
|
||||
return <>{children}</>
|
||||
}
|
||||
DropdownMenu.displayName = 'DropdownMenu'
|
||||
|
||||
// DropdownMenuTrigger
|
||||
interface DropdownMenuTriggerProps {
|
||||
children: ReactNode
|
||||
asChild?: boolean
|
||||
onClick?: (event: ReactMouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
const DropdownMenuTrigger = forwardRef<HTMLDivElement, DropdownMenuTriggerProps>(
|
||||
({ children, onClick, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
className="dropdown-menu-trigger"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuTrigger.displayName = 'DropdownMenuTrigger'
|
||||
|
||||
// DropdownMenuContent
|
||||
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, className, ...props }, ref) => {
|
||||
return (
|
||||
<Menu
|
||||
ref={ref}
|
||||
open={Boolean(anchorEl)}
|
||||
anchorEl={anchorEl}
|
||||
onClose={onClose}
|
||||
className={`dropdown-menu-content ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuContent.displayName = 'DropdownMenuContent'
|
||||
|
||||
// DropdownMenuItem
|
||||
export interface DropdownMenuItemProps {
|
||||
children?: ReactNode
|
||||
inset?: boolean
|
||||
icon?: ReactNode
|
||||
shortcut?: string
|
||||
onClick?: () => void
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const DropdownMenuItem = forwardRef<HTMLButtonElement, DropdownMenuItemProps>(
|
||||
({ children, inset, icon, shortcut, className, ...props }, ref) => {
|
||||
return (
|
||||
<MenuItem
|
||||
ref={ref}
|
||||
className={`dropdown-menu-item ${inset ? 'dropdown-menu-item--inset' : ''} ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{icon && <ListItemIcon>{icon}</ListItemIcon>}
|
||||
<ListItemText>{children}</ListItemText>
|
||||
{shortcut && (
|
||||
<Typography variant="caption" className="dropdown-menu-shortcut">
|
||||
{shortcut}
|
||||
</Typography>
|
||||
)}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem.displayName = 'DropdownMenuItem'
|
||||
|
||||
// DropdownMenuCheckboxItem
|
||||
interface DropdownMenuCheckboxItemProps {
|
||||
children?: ReactNode
|
||||
checked?: boolean
|
||||
onCheckedChange?: (checked: boolean) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const DropdownMenuCheckboxItem = forwardRef<HTMLButtonElement, DropdownMenuCheckboxItemProps>(
|
||||
({ children, checked, onCheckedChange, className, ...props }, ref) => {
|
||||
return (
|
||||
<MenuItem
|
||||
ref={ref}
|
||||
onClick={() => onCheckedChange?.(!checked)}
|
||||
className={`dropdown-menu-checkbox-item ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
<ListItemIcon>
|
||||
{checked && <Check size={16} />}
|
||||
</ListItemIcon>
|
||||
<ListItemText>{children}</ListItemText>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuCheckboxItem.displayName = 'DropdownMenuCheckboxItem'
|
||||
|
||||
// DropdownMenuLabel
|
||||
const DropdownMenuLabel = forwardRef<
|
||||
HTMLDivElement,
|
||||
{ children: ReactNode; inset?: boolean; className?: string }
|
||||
>(({ children, inset, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className={`dropdown-menu-label ${inset ? 'dropdown-menu-label--inset' : ''} ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
DropdownMenuLabel.displayName = 'DropdownMenuLabel'
|
||||
|
||||
// DropdownMenuSeparator
|
||||
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" className="dropdown-menu-shortcut">
|
||||
{children}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
||||
|
||||
// DropdownMenuGroup
|
||||
const DropdownMenuGroup = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`dropdown-menu-group ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuGroup.displayName = 'DropdownMenuGroup'
|
||||
|
||||
// DropdownMenuSub (submenu)
|
||||
const DropdownMenuSub = ({ children }: { children: ReactNode }) => <>{children}</>
|
||||
DropdownMenuSub.displayName = 'DropdownMenuSub'
|
||||
|
||||
// DropdownMenuSubTrigger
|
||||
const DropdownMenuSubTrigger = forwardRef<
|
||||
HTMLButtonElement,
|
||||
{ children: ReactNode; inset?: boolean; className?: string }
|
||||
>(({ children, inset, className, ...props }, ref) => {
|
||||
return (
|
||||
<MenuItem
|
||||
ref={ref}
|
||||
className={`dropdown-menu-sub-trigger ${inset ? 'dropdown-menu-sub-trigger--inset' : ''} ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
<ListItemText>{children}</ListItemText>
|
||||
<ChevronRight size={16} />
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
DropdownMenuSubTrigger.displayName = 'DropdownMenuSubTrigger'
|
||||
|
||||
// DropdownMenuSubContent
|
||||
const DropdownMenuSubContent = forwardRef<
|
||||
HTMLDivElement,
|
||||
{ children: ReactNode; className?: string }
|
||||
>(({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`dropdown-menu-sub-content ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
DropdownMenuSubContent.displayName = 'DropdownMenuSubContent'
|
||||
|
||||
// DropdownMenuPortal
|
||||
const DropdownMenuPortal = ({ children }: { children: ReactNode }) => <>{children}</>
|
||||
DropdownMenuPortal.displayName = 'DropdownMenuPortal'
|
||||
|
||||
// DropdownMenuRadioGroup
|
||||
const DropdownMenuRadioGroup = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
children: ReactNode
|
||||
value?: string
|
||||
onValueChange?: (value: string) => void
|
||||
className?: string
|
||||
}
|
||||
>(({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`dropdown-menu-radio-group ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
DropdownMenuRadioGroup.displayName = 'DropdownMenuRadioGroup'
|
||||
|
||||
// DropdownMenuRadioItem
|
||||
const DropdownMenuRadioItem = forwardRef<
|
||||
HTMLButtonElement,
|
||||
{ children: ReactNode; value: string; className?: string }
|
||||
>(({ children, value, className, ...props }, ref) => {
|
||||
return (
|
||||
<MenuItem ref={ref} className={`dropdown-menu-radio-item ${className || ''}`} {...props}>
|
||||
<ListItemIcon>
|
||||
<Box className="dropdown-menu-radio-dot" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{children}</ListItemText>
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
DropdownMenuRadioItem.displayName = 'DropdownMenuRadioItem'
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box, Popover as FakeMuiPopover } from '@/fakemui'
|
||||
|
||||
// Popover container
|
||||
export interface PopoverProps {
|
||||
children: ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
const Popover = ({ children, open, onOpenChange }: PopoverProps) => {
|
||||
return <>{children}</>
|
||||
}
|
||||
Popover.displayName = 'Popover'
|
||||
|
||||
// PopoverTrigger
|
||||
interface PopoverTriggerProps {
|
||||
children: ReactNode
|
||||
asChild?: boolean
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
const PopoverTrigger = forwardRef<HTMLDivElement, PopoverTriggerProps>(
|
||||
({ children, onClick, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
className="popover-trigger"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
PopoverTrigger.displayName = 'PopoverTrigger'
|
||||
|
||||
// PopoverContent
|
||||
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, className, ...props },
|
||||
ref
|
||||
) => {
|
||||
const anchorOrigin = {
|
||||
vertical: side === 'top' ? 'top' : 'bottom',
|
||||
horizontal: align === 'end' ? 'right' : align === 'center' ? 'center' : 'left',
|
||||
} as const
|
||||
|
||||
return (
|
||||
<FakeMuiPopover
|
||||
ref={ref}
|
||||
open={Boolean(anchorEl)}
|
||||
anchorEl={anchorEl}
|
||||
onClose={onClose}
|
||||
anchorOrigin={anchorOrigin}
|
||||
transformOrigin={{
|
||||
vertical: side === 'top' ? 'bottom' : 'top',
|
||||
horizontal: anchorOrigin.horizontal,
|
||||
}}
|
||||
className={`popover-content ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</FakeMuiPopover>
|
||||
)
|
||||
}
|
||||
)
|
||||
PopoverContent.displayName = 'PopoverContent'
|
||||
|
||||
// PopoverAnchor
|
||||
const PopoverAnchor = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className="popover-anchor" {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
PopoverAnchor.displayName = 'PopoverAnchor'
|
||||
|
||||
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }
|
||||
@@ -1,111 +0,0 @@
|
||||
# Organisms
|
||||
|
||||
Complex UI sections that combine atoms and molecules into complete features. Built on Material UI.
|
||||
|
||||
## Layout Components
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| `Table` | Data table with header/body/footer |
|
||||
| `Command` | Command palette (cmdk-style) |
|
||||
| `Sheet` | Side panel drawer |
|
||||
| `Sidebar` | Navigation sidebar with groups |
|
||||
| `NavigationMenu` | Top navigation with dropdowns |
|
||||
| `Form` | Form with react-hook-form integration |
|
||||
|
||||
## Feature Components
|
||||
|
||||
### Core Builders
|
||||
- `SchemaEditor` - Full schema editing interface
|
||||
- `ComponentCatalog` - Component browsing and selection
|
||||
- `PropertyInspector` - Component property editor
|
||||
- `Builder` - Visual page builder with drag-and-drop
|
||||
- `Canvas` - Rendering canvas for visual builder
|
||||
|
||||
### Configuration Managers
|
||||
- `CssClassBuilder` - Visual CSS class selection
|
||||
- `ThemeEditor` - Theme customization interface
|
||||
- `SMTPConfigEditor` - SMTP configuration form
|
||||
|
||||
### Code Editors
|
||||
- `CodeEditor` - Monaco-based code editor
|
||||
- `LuaEditor` - Lua script editor
|
||||
- `JsonEditor` - JSON editing with validation
|
||||
- `NerdModeIDE` - Full-featured IDE panel
|
||||
|
||||
### Data Management
|
||||
- `DatabaseManager` - Database management
|
||||
- `UserManagement` - User CRUD interface
|
||||
- `PackageManager` - Package management
|
||||
- `AuditLogViewer` - Audit log display
|
||||
|
||||
### Security Components
|
||||
- `SecurityWarningDialog` - Security scan results dialog with severity classification
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Table, TableHeader, TableBody, TableRow, TableCell,
|
||||
Sidebar, SidebarHeader, SidebarContent,
|
||||
UserManagement
|
||||
} from '@/components/organisms'
|
||||
|
||||
function AdminPanel() {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', height: '100vh' }}>
|
||||
<Sidebar width={280}>
|
||||
<SidebarHeader>Admin</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup label="Users">
|
||||
<SidebarMenuItem icon={<PeopleIcon />}>Users</SidebarMenuItem>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
<Box sx={{ flex: 1, p: 3 }}>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map(user => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.name}</TableCell>
|
||||
<TableCell>{user.status}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. ✅ **DO** compose organisms from molecules and atoms
|
||||
2. ✅ **DO** include business logic when needed
|
||||
3. ✅ **DO** handle data fetching and state management
|
||||
4. ✅ **DO** use MUI `sx` prop for styling
|
||||
5. ❌ **DON'T** use Tailwind classes
|
||||
6. ❌ **DON'T** create organisms that are too granular (use molecules)
|
||||
|
||||
## Testing
|
||||
|
||||
```typescript
|
||||
describe('UserManagement', () => {
|
||||
it('allows creating new users', async () => {
|
||||
render(<UserManagement />)
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /add user/i }))
|
||||
await userEvent.type(screen.getByLabelText(/username/i), 'newuser')
|
||||
await userEvent.click(screen.getByRole('button', { name: /save/i }))
|
||||
|
||||
expect(screen.getByText('newuser')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
```
|
||||
@@ -1,143 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useForm,
|
||||
useFormContext,
|
||||
UseFormReturn,
|
||||
} from 'react-hook-form'
|
||||
|
||||
import {
|
||||
Box,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
} from '@/fakemui'
|
||||
|
||||
// Form wrapper with react-hook-form
|
||||
interface FormProps<T extends FieldValues> {
|
||||
form: UseFormReturn<T>
|
||||
onSubmit: (data: T) => void
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
function Form<T extends FieldValues>({ form, onSubmit, children, className, ...props }: FormProps<T>) {
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className={`form-wrapper ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
Form.displayName = 'Form'
|
||||
|
||||
// FormField (wrapper for Controller)
|
||||
interface FormFieldProps<T extends FieldValues> extends Omit<ControllerProps<T>, 'render'> {
|
||||
render: ControllerProps<T>['render']
|
||||
}
|
||||
|
||||
function FormField<T extends FieldValues>({ name, control, render, ...props }: FormFieldProps<T>) {
|
||||
return <Controller name={name} control={control} render={render} {...props} />
|
||||
}
|
||||
FormField.displayName = 'FormField'
|
||||
|
||||
// FormItem
|
||||
const FormItem = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`form-item ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
FormItem.displayName = 'FormItem'
|
||||
|
||||
// FormLabel
|
||||
const FormLabelComponent = forwardRef<
|
||||
HTMLLabelElement,
|
||||
{ children: ReactNode; required?: boolean; error?: boolean; className?: string }
|
||||
>(({ children, required, error, className, ...props }, ref) => {
|
||||
return (
|
||||
<FormLabel
|
||||
ref={ref}
|
||||
required={required}
|
||||
error={error}
|
||||
className={`form-label ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</FormLabel>
|
||||
)
|
||||
})
|
||||
FormLabelComponent.displayName = 'FormLabel'
|
||||
|
||||
// FormControl (wrapper)
|
||||
const FormControlComponent = forwardRef<
|
||||
HTMLDivElement,
|
||||
{ children: ReactNode; fullWidth?: boolean; error?: boolean; className?: string }
|
||||
>(({ children, fullWidth = true, error, className, ...props }, ref) => {
|
||||
return (
|
||||
<FormControl ref={ref} fullWidth={fullWidth} error={error} className={className} {...props}>
|
||||
{children}
|
||||
</FormControl>
|
||||
)
|
||||
})
|
||||
FormControlComponent.displayName = 'FormControl'
|
||||
|
||||
// FormDescription
|
||||
const FormDescription = forwardRef<
|
||||
HTMLParagraphElement,
|
||||
{ children: ReactNode; className?: string }
|
||||
>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<FormHelperText ref={ref} {...props}>
|
||||
{children}
|
||||
</FormHelperText>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = 'FormDescription'
|
||||
|
||||
// FormMessage (error message)
|
||||
const FormMessage = forwardRef<HTMLParagraphElement, { children?: ReactNode; className?: string }>(
|
||||
({ children, ...props }, ref) => {
|
||||
if (!children) return null
|
||||
return (
|
||||
<FormHelperText ref={ref} error {...props}>
|
||||
{children}
|
||||
</FormHelperText>
|
||||
)
|
||||
}
|
||||
)
|
||||
FormMessage.displayName = 'FormMessage'
|
||||
|
||||
// useFormField hook (to get form context in nested components)
|
||||
const useFormField = () => {
|
||||
const context = useFormContext()
|
||||
return context
|
||||
}
|
||||
|
||||
export {
|
||||
Form,
|
||||
FormControlComponent as FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabelComponent as FormLabel,
|
||||
FormMessage,
|
||||
FormProvider,
|
||||
useForm,
|
||||
useFormField,
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Table barrel export - maintains backward compatibility after splitting into smaller organisms
|
||||
// Components split into separate files to keep each under 150 LOC
|
||||
export {
|
||||
TableCaption,
|
||||
TableCell,
|
||||
type TableCellProps,
|
||||
TableHead,
|
||||
type TableHeadProps,
|
||||
TableRow,
|
||||
type TableRowProps,
|
||||
} from './TableCell'
|
||||
export {
|
||||
TableCore as Table,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHeader,
|
||||
type TableProps,
|
||||
} from './TableCore'
|
||||
@@ -1,98 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { ComponentProps,forwardRef, ReactNode } from 'react'
|
||||
|
||||
import {
|
||||
TableCell as FakemuiTableCell,
|
||||
TableRow as FakemuiTableRow,
|
||||
} from '@/fakemui'
|
||||
|
||||
// TableRow
|
||||
export interface TableRowProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
selected?: boolean
|
||||
hover?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(
|
||||
({ children, selected, hover = true, className, ...props }, ref) => {
|
||||
const classes = [
|
||||
'table-row',
|
||||
selected && 'table-row--selected',
|
||||
hover && 'table-row--hover',
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
return (
|
||||
<FakemuiTableRow ref={ref} className={classes} {...props}>
|
||||
{children}
|
||||
</FakemuiTableRow>
|
||||
)
|
||||
}
|
||||
)
|
||||
TableRow.displayName = 'TableRow'
|
||||
|
||||
// TableHead (cell in header)
|
||||
export interface TableHeadProps extends ComponentProps<typeof FakemuiTableCell> {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TableHead = forwardRef<HTMLTableCellElement, TableHeadProps>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<FakemuiTableCell
|
||||
ref={ref}
|
||||
component="th"
|
||||
className={`table-head-cell ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</FakemuiTableCell>
|
||||
)
|
||||
}
|
||||
)
|
||||
TableHead.displayName = 'TableHead'
|
||||
|
||||
// TableCell
|
||||
export interface TableCellProps extends ComponentProps<typeof FakemuiTableCell> {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<FakemuiTableCell ref={ref} {...props}>
|
||||
{children}
|
||||
</FakemuiTableCell>
|
||||
)
|
||||
}
|
||||
)
|
||||
TableCell.displayName = 'TableCell'
|
||||
|
||||
// TableCaption
|
||||
const TableCaption = forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
{ children: ReactNode; className?: string }
|
||||
>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<caption
|
||||
ref={ref}
|
||||
style={{
|
||||
captionSide: 'bottom',
|
||||
padding: '12px',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--text-secondary)',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</caption>
|
||||
)
|
||||
})
|
||||
TableCaption.displayName = 'TableCaption'
|
||||
|
||||
export { TableCaption, TableCell, TableHead, TableRow }
|
||||
@@ -1,74 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import {
|
||||
Paper,
|
||||
Table,
|
||||
TableBody as FakemuiTableBody,
|
||||
TableContainer,
|
||||
TableFooter as FakemuiTableFooter,
|
||||
TableHead as FakemuiTableHead,
|
||||
} from '@/fakemui'
|
||||
|
||||
// Table wrapper with container
|
||||
export interface TableProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
stickyHeader?: boolean
|
||||
}
|
||||
|
||||
const TableCore = forwardRef<HTMLTableElement, TableProps>(
|
||||
({ children, stickyHeader, className, ...props }, ref) => {
|
||||
return (
|
||||
<TableContainer>
|
||||
<Paper className={`table-wrapper ${className || ''}`}>
|
||||
<Table ref={ref} stickyHeader={stickyHeader} size="small" {...props}>
|
||||
{children}
|
||||
</Table>
|
||||
</Paper>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
)
|
||||
TableCore.displayName = 'TableCore'
|
||||
|
||||
// TableHeader
|
||||
const TableHeader = forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
{ children: ReactNode; className?: string }
|
||||
>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<FakemuiTableHead ref={ref} {...props}>
|
||||
{children}
|
||||
</FakemuiTableHead>
|
||||
)
|
||||
})
|
||||
TableHeader.displayName = 'TableHeader'
|
||||
|
||||
// TableBody
|
||||
const TableBody = forwardRef<HTMLTableSectionElement, { children: ReactNode; className?: string }>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<FakemuiTableBody ref={ref} {...props}>
|
||||
{children}
|
||||
</FakemuiTableBody>
|
||||
)
|
||||
}
|
||||
)
|
||||
TableBody.displayName = 'TableBody'
|
||||
|
||||
// TableFooter
|
||||
const TableFooter = forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
{ children: ReactNode; className?: string }
|
||||
>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<FakemuiTableFooter ref={ref} {...props}>
|
||||
{children}
|
||||
</FakemuiTableFooter>
|
||||
)
|
||||
})
|
||||
TableFooter.displayName = 'TableFooter'
|
||||
|
||||
export { TableBody, TableCore, TableFooter, TableHeader }
|
||||
@@ -1,136 +0,0 @@
|
||||
// Organisms - Complex UI sections
|
||||
// These are larger components that combine atoms and molecules
|
||||
|
||||
// New MUI-based organisms - Data
|
||||
export {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
type TableCellProps,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
type TableHeadProps,
|
||||
type TableProps,
|
||||
TableRow,
|
||||
type TableRowProps,
|
||||
} from './data/Table'
|
||||
|
||||
// New MUI-based organisms - Overlay/Dialogs
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandLoading,
|
||||
type CommandProps,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from './overlay/Command'
|
||||
export {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetOverlay,
|
||||
SheetPortal,
|
||||
type SheetProps,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from './overlay/Sheet'
|
||||
|
||||
// New MUI-based organisms - Navigation
|
||||
export {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField as FormFieldController,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormProvider,
|
||||
useForm,
|
||||
useFormField,
|
||||
} from './data/Form'
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
type NavigationMenuProps,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenuViewport,
|
||||
} from './navigation/NavigationMenu'
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
type SidebarProps,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
} from './navigation/Sidebar'
|
||||
|
||||
// Legacy feature components
|
||||
export { AuditLogViewer } from '../AuditLogViewer'
|
||||
export { Builder } from '../Builder'
|
||||
export { Canvas } from '../Canvas'
|
||||
export { CodeEditor } from '../CodeEditor'
|
||||
export { ComponentCatalog } from '../ComponentCatalog'
|
||||
export { ComponentConfigDialog } from '../ComponentConfigDialog'
|
||||
export { ComponentHierarchyEditor } from '../ComponentHierarchyEditor'
|
||||
export { CssClassBuilder } from '../CssClassBuilder'
|
||||
export { CssClassManager } from '../CssClassManager'
|
||||
export { DatabaseManager } from '../DatabaseManager'
|
||||
export { DropdownConfigManager } from '../DropdownConfigManager'
|
||||
export { FieldRenderer } from '../FieldRenderer'
|
||||
export { GenericPage } from '../GenericPage'
|
||||
export { GitHubActionsFetcher } from '../GitHubActionsFetcher'
|
||||
export { GodCredentialsSettings } from '../GodCredentialsSettings'
|
||||
export { JsonEditor } from '../JsonEditor'
|
||||
export { ContactSection } from '../level1/ContactSection'
|
||||
export { FeaturesSection } from '../level1/FeaturesSection'
|
||||
export { HeroSection } from '../level1/HeroSection'
|
||||
export { NavigationBar } from '../level1/NavigationBar'
|
||||
export { CommentsList } from '../level2/CommentsList'
|
||||
export { Login } from '../Login'
|
||||
export { LuaEditor } from '../LuaEditor'
|
||||
export { LuaSnippetLibrary } from '../LuaSnippetLibrary'
|
||||
export { ModelListView } from '../ModelListView'
|
||||
export { NerdModeIDE } from '../NerdModeIDE'
|
||||
export { PackageImportExport } from '../PackageImportExport'
|
||||
export { PackageManager } from '../PackageManager'
|
||||
export { PageRoutesManager } from '../PageRoutesManager'
|
||||
export { PropertyInspector } from '../PropertyInspector'
|
||||
export { QuickGuide } from '../QuickGuide'
|
||||
export { RecordForm } from '../RecordForm'
|
||||
export { RenderComponent } from '../RenderComponent'
|
||||
export { SchemaEditor } from '../SchemaEditor'
|
||||
export { SchemaEditorLevel4 } from '../SchemaEditorLevel4'
|
||||
export { ScreenshotAnalyzer } from '../ScreenshotAnalyzer'
|
||||
export { SMTPConfigEditor } from '../SMTPConfigEditor'
|
||||
export { ThemeEditor } from '../ThemeEditor'
|
||||
export { UnifiedLogin } from '../UnifiedLogin'
|
||||
export { UserManagement } from '../UserManagement'
|
||||
export { WorkflowEditor } from '../WorkflowEditor'
|
||||
|
||||
// Security components
|
||||
export { SecurityWarningDialog } from './security/SecurityWarningDialog'
|
||||
@@ -1,15 +0,0 @@
|
||||
// NavigationMenu barrel export - maintains backward compatibility after splitting into smaller organisms
|
||||
// Components split into separate files to keep each under 150 LOC
|
||||
export {
|
||||
NavigationMenuCore as NavigationMenu,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuList,
|
||||
type NavigationMenuProps,
|
||||
} from './NavigationMenuCore'
|
||||
export {
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuLink,
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenuViewport,
|
||||
} from './NavigationMenuLink'
|
||||
export { NavigationMenuContent, NavigationMenuTrigger } from './NavigationMenuTrigger'
|
||||
@@ -1,31 +0,0 @@
|
||||
@use '@/styles/variables' as *;
|
||||
@use '@/styles/mixins' as *;
|
||||
|
||||
.navigationMenu {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.navigationMenuList {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navigationMenuItem {
|
||||
position: relative;
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box } from '@/fakemui'
|
||||
|
||||
import styles from './NavigationMenuCore.module.scss'
|
||||
|
||||
// NavigationMenu container
|
||||
export interface NavigationMenuProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
orientation?: 'horizontal' | 'vertical'
|
||||
}
|
||||
|
||||
const NavigationMenuCore = forwardRef<HTMLDivElement, NavigationMenuProps>(
|
||||
({ children, className = '', orientation = 'horizontal', ...props }, ref) => {
|
||||
const orientationClass = orientation === 'horizontal'
|
||||
? styles.horizontal
|
||||
: styles.vertical
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className={`${styles.navigationMenu} ${orientationClass} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
NavigationMenuCore.displayName = 'NavigationMenuCore'
|
||||
|
||||
// NavigationMenuList
|
||||
const NavigationMenuList = forwardRef<
|
||||
HTMLUListElement,
|
||||
{ children: ReactNode; className?: string }
|
||||
>(({ children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
component="ul"
|
||||
className={`${styles.navigationMenuList} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
NavigationMenuList.displayName = 'NavigationMenuList'
|
||||
|
||||
// NavigationMenuItem
|
||||
interface NavigationMenuItemProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const NavigationMenuItem = forwardRef<HTMLLIElement, NavigationMenuItemProps>(
|
||||
({ children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
component="li"
|
||||
className={`${styles.navigationMenuItem} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
NavigationMenuItem.displayName = 'NavigationMenuItem'
|
||||
|
||||
export { NavigationMenuCore, NavigationMenuItem, NavigationMenuList }
|
||||
@@ -1,62 +0,0 @@
|
||||
@use '@/styles/variables' as *;
|
||||
@use '@/styles/mixins' as *;
|
||||
|
||||
.navigationMenuLink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
min-width: auto;
|
||||
padding: 0.5rem 1rem;
|
||||
color: var(--text-primary, inherit);
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--action-hover, rgba(0, 0, 0, 0.04));
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--primary-main, #1976d2);
|
||||
}
|
||||
}
|
||||
|
||||
.navigationMenuIndicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 2px;
|
||||
background-color: var(--primary-main, #1976d2);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.navigationMenuViewport {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--background-paper, #fff);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--shadow-3, 0 4px 20px rgba(0, 0, 0, 0.15));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.triggerStyle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--action-hover, rgba(0, 0, 0, 0.04));
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: var(--action-focus, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box, Button } from '@/fakemui'
|
||||
|
||||
import styles from './NavigationMenuLink.module.scss'
|
||||
|
||||
// NavigationMenuLink
|
||||
interface NavigationMenuLinkProps {
|
||||
children: ReactNode
|
||||
href?: string
|
||||
active?: boolean
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const NavigationMenuLink = forwardRef<HTMLAnchorElement, NavigationMenuLinkProps>(
|
||||
({ children, href, active, onClick, className = '', ...props }, ref) => {
|
||||
const activeClass = active ? styles.active : ''
|
||||
|
||||
// Use anchor element if href is provided
|
||||
if (href) {
|
||||
return (
|
||||
<a
|
||||
ref={ref}
|
||||
href={href}
|
||||
onClick={onClick}
|
||||
className={`${styles.navigationMenuLink} ${activeClass} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref as React.Ref<HTMLButtonElement>}
|
||||
onClick={onClick}
|
||||
variant="ghost"
|
||||
className={`${styles.navigationMenuLink} ${activeClass} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
)
|
||||
NavigationMenuLink.displayName = 'NavigationMenuLink'
|
||||
|
||||
// NavigationMenuIndicator
|
||||
const NavigationMenuIndicator = forwardRef<HTMLDivElement, { className?: string }>(
|
||||
({ className = '', ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className={`${styles.navigationMenuIndicator} ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
NavigationMenuIndicator.displayName = 'NavigationMenuIndicator'
|
||||
|
||||
// NavigationMenuViewport
|
||||
const NavigationMenuViewport = forwardRef<
|
||||
HTMLDivElement,
|
||||
{ children?: ReactNode; className?: string }
|
||||
>(({ children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className={`${styles.navigationMenuViewport} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
NavigationMenuViewport.displayName = 'NavigationMenuViewport'
|
||||
|
||||
// Helper: navigationMenuTriggerStyle (returns className for consistent styling)
|
||||
const navigationMenuTriggerStyle = () => styles.triggerStyle
|
||||
|
||||
export {
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuLink,
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenuViewport,
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
@use '@/styles/variables' as *;
|
||||
@use '@/styles/mixins' as *;
|
||||
|
||||
.navigationMenuTrigger {
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, inherit);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--action-hover, rgba(0, 0, 0, 0.04));
|
||||
}
|
||||
}
|
||||
|
||||
.navigationMenuContent {
|
||||
margin-top: 0.5rem;
|
||||
min-width: 200px;
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--shadow-3, 0 4px 20px rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Button, Menu } from '@/fakemui'
|
||||
import { KeyboardArrowDown } from '@/fakemui/icons'
|
||||
|
||||
import styles from './NavigationMenuTrigger.module.scss'
|
||||
|
||||
// NavigationMenuTrigger
|
||||
interface NavigationMenuTriggerProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
const NavigationMenuTrigger = forwardRef<HTMLButtonElement, NavigationMenuTriggerProps>(
|
||||
({ children, className = '', onClick, ...props }, ref) => {
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
variant="ghost"
|
||||
endIcon={<KeyboardArrowDown />}
|
||||
className={`${styles.navigationMenuTrigger} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
)
|
||||
NavigationMenuTrigger.displayName = 'NavigationMenuTrigger'
|
||||
|
||||
// NavigationMenuContent
|
||||
interface NavigationMenuContentProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
anchorEl?: HTMLElement | null
|
||||
open?: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const NavigationMenuContent = forwardRef<HTMLDivElement, NavigationMenuContentProps>(
|
||||
({ children, className = '', anchorEl: _anchorEl, open, onClose, ...props }, ref) => {
|
||||
return (
|
||||
<Menu
|
||||
ref={ref}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
className={`${styles.navigationMenuContent} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
)
|
||||
NavigationMenuContent.displayName = 'NavigationMenuContent'
|
||||
|
||||
export { NavigationMenuContent, NavigationMenuTrigger }
|
||||
@@ -1,7 +0,0 @@
|
||||
// Sidebar barrel export - maintains backward compatibility after splitting into smaller organisms
|
||||
// Components split into separate files to keep each under 150 LOC
|
||||
export { SidebarCore as Sidebar, type SidebarProps } from './SidebarCore'
|
||||
export { SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger } from './SidebarExtras'
|
||||
export { SidebarGroup, SidebarGroupContent, SidebarGroupLabel } from './SidebarGroup'
|
||||
export { SidebarContent, SidebarFooter, SidebarHeader, SidebarInset } from './SidebarLayout'
|
||||
export { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from './SidebarMenu'
|
||||
@@ -1,32 +0,0 @@
|
||||
@use '@/styles/variables' as *;
|
||||
@use '@/styles/mixins' as *;
|
||||
|
||||
.sidebarDrawer {
|
||||
:global(.drawer__content) {
|
||||
width: var(--sidebar-width, 280px);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarCore {
|
||||
flex-shrink: 0;
|
||||
width: var(--sidebar-width, 280px);
|
||||
transition: width 0.2s ease-in-out;
|
||||
overflow: hidden;
|
||||
|
||||
&.open {
|
||||
width: var(--sidebar-width, 280px);
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
width: var(--sidebar-width, 64px);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarInner {
|
||||
width: var(--sidebar-inner-width, 280px);
|
||||
height: 100%;
|
||||
background-color: var(--background-paper, #fff);
|
||||
border-right: 1px solid var(--divider, rgba(0, 0, 0, 0.12));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box, Drawer, useMediaQueryDown } from '@/fakemui'
|
||||
|
||||
import styles from './SidebarCore.module.scss'
|
||||
|
||||
// Sidebar container
|
||||
export interface SidebarProps {
|
||||
children: ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
width?: number
|
||||
collapsedWidth?: number
|
||||
variant?: 'permanent' | 'persistent' | 'temporary'
|
||||
side?: 'left' | 'right'
|
||||
}
|
||||
|
||||
const SidebarCore = forwardRef<HTMLDivElement, SidebarProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
open = true,
|
||||
onOpenChange,
|
||||
width = 280,
|
||||
collapsedWidth = 64,
|
||||
variant = 'permanent',
|
||||
side = 'left',
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const isMobile = useMediaQueryDown('md')
|
||||
|
||||
if (isMobile || variant === 'temporary') {
|
||||
return (
|
||||
<Drawer
|
||||
ref={ref}
|
||||
anchor={side}
|
||||
open={open}
|
||||
onClose={() => onOpenChange?.(false)}
|
||||
className={styles.sidebarDrawer}
|
||||
style={{ '--sidebar-width': `${width}px` } as React.CSSProperties}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
const sidebarWidth = open ? width : collapsedWidth
|
||||
const openClass = open ? styles.open : styles.collapsed
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
component="nav"
|
||||
className={`${styles.sidebarCore} ${openClass}`}
|
||||
style={{ '--sidebar-width': `${sidebarWidth}px`, '--sidebar-inner-width': `${width}px` } as React.CSSProperties}
|
||||
{...props}
|
||||
>
|
||||
<Box className={styles.sidebarInner}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarCore.displayName = 'SidebarCore'
|
||||
|
||||
export { SidebarCore }
|
||||
@@ -1,24 +0,0 @@
|
||||
@use '@/styles/variables' as *;
|
||||
@use '@/styles/mixins' as *;
|
||||
|
||||
.sidebarSeparator {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.sidebarTrigger {
|
||||
// Uses IconButton styles
|
||||
}
|
||||
|
||||
.sidebarRail {
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 8px;
|
||||
cursor: col-resize;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary-main, #1976d2);
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box, Divider, IconButton } from '@/fakemui'
|
||||
import { Menu } from '@/fakemui/icons'
|
||||
|
||||
import styles from './SidebarExtras.module.scss'
|
||||
|
||||
// SidebarSeparator
|
||||
const SidebarSeparator = forwardRef<HTMLHRElement, { className?: string }>(
|
||||
({ className = '', ...props }, ref) => {
|
||||
return <Divider ref={ref} className={`${styles.sidebarSeparator} ${className}`} {...props} />
|
||||
}
|
||||
)
|
||||
SidebarSeparator.displayName = 'SidebarSeparator'
|
||||
|
||||
// SidebarTrigger
|
||||
interface SidebarTriggerProps {
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SidebarTrigger = forwardRef<HTMLButtonElement, SidebarTriggerProps>(
|
||||
({ onClick, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<IconButton ref={ref} onClick={onClick} sm className={`${styles.sidebarTrigger} ${className}`} {...props}>
|
||||
<Menu />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarTrigger.displayName = 'SidebarTrigger'
|
||||
|
||||
// SidebarRail
|
||||
const SidebarRail = forwardRef<HTMLDivElement, { className?: string }>(
|
||||
({ className = '', ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className={`${styles.sidebarRail} ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarRail.displayName = 'SidebarRail'
|
||||
|
||||
// SidebarProvider
|
||||
const SidebarProvider = ({ children }: { children: ReactNode }) => <>{children}</>
|
||||
SidebarProvider.displayName = 'SidebarProvider'
|
||||
|
||||
export { SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger }
|
||||
@@ -1,41 +0,0 @@
|
||||
@use '@/styles/variables' as *;
|
||||
@use '@/styles/mixins' as *;
|
||||
|
||||
.sidebarGroup {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebarGroupHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&.collapsible {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--action-hover, rgba(0, 0, 0, 0.04));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarGroupLabel {
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
.sidebarGroupLabelStandalone {
|
||||
display: block;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
.sidebarGroupList {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode, useState } from 'react'
|
||||
|
||||
import { Box, Collapse, List, Typography } from '@/fakemui'
|
||||
import { ExpandLess, ExpandMore } from '@/fakemui/icons'
|
||||
|
||||
import styles from './SidebarGroup.module.scss'
|
||||
|
||||
// SidebarGroup
|
||||
interface SidebarGroupProps {
|
||||
children: ReactNode
|
||||
label?: string
|
||||
collapsible?: boolean
|
||||
defaultOpen?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SidebarGroup = forwardRef<HTMLDivElement, SidebarGroupProps>(
|
||||
({ children, label, collapsible, defaultOpen = true, className = '', ...props }, ref) => {
|
||||
const [open, setOpen] = useState(defaultOpen)
|
||||
|
||||
return (
|
||||
<Box ref={ref} className={`${styles.sidebarGroup} ${className}`} {...props}>
|
||||
{label && (
|
||||
<Box
|
||||
onClick={collapsible ? () => setOpen(!open) : undefined}
|
||||
className={`${styles.sidebarGroupHeader} ${collapsible ? styles.collapsible : ''}`}
|
||||
>
|
||||
<Typography
|
||||
variant="overline"
|
||||
className={styles.sidebarGroupLabel}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
{collapsible &&
|
||||
(open ? <ExpandLess size={16} /> : <ExpandMore size={16} />)}
|
||||
</Box>
|
||||
)}
|
||||
{collapsible ? (
|
||||
<Collapse in={open}>
|
||||
<List dense className={styles.sidebarGroupList}>
|
||||
{children}
|
||||
</List>
|
||||
</Collapse>
|
||||
) : (
|
||||
<List dense className={styles.sidebarGroupList}>
|
||||
{children}
|
||||
</List>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarGroup.displayName = 'SidebarGroup'
|
||||
|
||||
// SidebarGroupLabel
|
||||
const SidebarGroupLabel = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<Typography
|
||||
ref={ref}
|
||||
variant="overline"
|
||||
className={`${styles.sidebarGroupLabelStandalone} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarGroupLabel.displayName = 'SidebarGroupLabel'
|
||||
|
||||
// SidebarGroupContent
|
||||
const SidebarGroupContent = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={className} {...props}>
|
||||
<List dense className={styles.sidebarGroupList}>
|
||||
{children}
|
||||
</List>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarGroupContent.displayName = 'SidebarGroupContent'
|
||||
|
||||
export { SidebarGroup, SidebarGroupContent, SidebarGroupLabel }
|
||||
@@ -1,26 +0,0 @@
|
||||
@use '@/styles/variables' as *;
|
||||
@use '@/styles/mixins' as *;
|
||||
|
||||
.sidebarHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--divider, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
.sidebarContent {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.sidebarFooter {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid var(--divider, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
.sidebarInset {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box } from '@/fakemui'
|
||||
|
||||
import styles from './SidebarLayout.module.scss'
|
||||
|
||||
// SidebarHeader
|
||||
const SidebarHeader = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className={`${styles.sidebarHeader} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarHeader.displayName = 'SidebarHeader'
|
||||
|
||||
// SidebarContent
|
||||
const SidebarContent = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className={`${styles.sidebarContent} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarContent.displayName = 'SidebarContent'
|
||||
|
||||
// SidebarFooter
|
||||
const SidebarFooter = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className={`${styles.sidebarFooter} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarFooter.displayName = 'SidebarFooter'
|
||||
|
||||
// SidebarInset
|
||||
const SidebarInset = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`${styles.sidebarInset} ${className}`} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarInset.displayName = 'SidebarInset'
|
||||
|
||||
export { SidebarContent, SidebarFooter, SidebarHeader, SidebarInset }
|
||||
@@ -1,66 +0,0 @@
|
||||
@use '@/styles/variables' as *;
|
||||
@use '@/styles/mixins' as *;
|
||||
|
||||
.sidebarMenuList {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebarMenuListItem {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebarMenuItemWrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebarMenuButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: calc(100% - 0.5rem);
|
||||
margin: 0 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
text-align: left;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--action-hover, rgba(0, 0, 0, 0.04));
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:global(.list-item-button--selected) {
|
||||
background-color: var(--primary-main, #1976d2);
|
||||
color: var(--primary-contrast-text, #fff);
|
||||
|
||||
.sidebarMenuIcon {
|
||||
color: var(--primary-contrast-text, #fff);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary-dark, #1565c0);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarMenuIcon {
|
||||
min-width: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sidebarMenuText {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@/fakemui'
|
||||
|
||||
import styles from './SidebarMenu.module.scss'
|
||||
|
||||
// SidebarMenu (alias for List)
|
||||
const SidebarMenu = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={className} {...props}>
|
||||
<List dense className={styles.sidebarMenuList}>
|
||||
{children}
|
||||
</List>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarMenu.displayName = 'SidebarMenu'
|
||||
|
||||
// SidebarMenuItem
|
||||
interface SidebarMenuItemProps {
|
||||
children?: ReactNode
|
||||
icon?: ReactNode
|
||||
label?: string
|
||||
href?: string
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SidebarMenuItem = forwardRef<HTMLDivElement, SidebarMenuItemProps>(
|
||||
({ children, icon, label, href, active, disabled, onClick, className = '', ...props }, ref) => {
|
||||
const content = children || label
|
||||
const activeClass = active ? styles.active : ''
|
||||
|
||||
const buttonContent = (
|
||||
<>
|
||||
{icon && <ListItemIcon className={styles.sidebarMenuIcon}>{icon}</ListItemIcon>}
|
||||
<ListItemText primary={content} className={styles.sidebarMenuText} />
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<ListItem className={styles.sidebarMenuListItem}>
|
||||
<Box ref={ref} className={`${styles.sidebarMenuItemWrapper} ${className}`} {...props}>
|
||||
{href ? (
|
||||
<a
|
||||
href={href}
|
||||
onClick={onClick}
|
||||
className={`${styles.sidebarMenuButton} ${activeClass} ${disabled ? styles.disabled : ''}`}
|
||||
aria-disabled={disabled}
|
||||
>
|
||||
{buttonContent}
|
||||
</a>
|
||||
) : (
|
||||
<ListItemButton
|
||||
selected={active}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className={`${styles.sidebarMenuButton} ${activeClass}`}
|
||||
>
|
||||
{buttonContent}
|
||||
</ListItemButton>
|
||||
)}
|
||||
</Box>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarMenuItem.displayName = 'SidebarMenuItem'
|
||||
|
||||
// SidebarMenuButton (alias for ListItemButton)
|
||||
const SidebarMenuButton = forwardRef<HTMLDivElement, SidebarMenuItemProps>((props, ref) => (
|
||||
<SidebarMenuItem ref={ref} {...props} />
|
||||
))
|
||||
SidebarMenuButton.displayName = 'SidebarMenuButton'
|
||||
|
||||
export { SidebarMenu, SidebarMenuButton, SidebarMenuItem }
|
||||
@@ -1,6 +0,0 @@
|
||||
// Command barrel export - maintains backward compatibility after splitting into smaller organisms
|
||||
// Components split into separate files to keep each under 150 LOC
|
||||
export { CommandCore as Command, type CommandProps } from './CommandCore'
|
||||
export { CommandDialog, CommandInput } from './CommandDialog'
|
||||
export { CommandItem, CommandLoading, CommandSeparator, CommandShortcut } from './CommandItem'
|
||||
export { CommandEmpty, CommandGroup, CommandList } from './CommandList'
|
||||
@@ -1,33 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Paper } from '@/fakemui'
|
||||
|
||||
// Command container (like cmdk)
|
||||
export interface CommandProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
onValueChange?: (value: string) => void
|
||||
value?: string
|
||||
filter?: (value: string, search: string) => number
|
||||
shouldFilter?: boolean
|
||||
}
|
||||
|
||||
const CommandCore = forwardRef<HTMLDivElement, CommandProps>(
|
||||
({ children, onValueChange, value, shouldFilter = true, className, ...props }, ref) => {
|
||||
return (
|
||||
<Paper
|
||||
ref={ref}
|
||||
variant="outlined"
|
||||
className={`command-container ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
)
|
||||
CommandCore.displayName = 'CommandCore'
|
||||
|
||||
export { CommandCore }
|
||||
@@ -1,56 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box, Input } from '@/fakemui'
|
||||
import { Search } from '@/fakemui/icons'
|
||||
|
||||
// CommandDialog
|
||||
interface CommandDialogProps {
|
||||
children: ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
const CommandDialog = ({ children, open, onOpenChange }: CommandDialogProps) => {
|
||||
if (!open) return null
|
||||
return (
|
||||
<Box className="command-dialog-overlay" onClick={() => onOpenChange?.(false)}>
|
||||
<Box onClick={e => e.stopPropagation()} className="command-dialog-content">
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
CommandDialog.displayName = 'CommandDialog'
|
||||
|
||||
// CommandInput
|
||||
interface CommandInputProps {
|
||||
placeholder?: string
|
||||
value?: string
|
||||
onValueChange?: (value: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CommandInput = forwardRef<HTMLInputElement, CommandInputProps>(
|
||||
({ placeholder = 'Search...', value, onValueChange, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box className={`command-input-wrapper ${className || ''}`}>
|
||||
<Input
|
||||
ref={ref}
|
||||
fullWidth
|
||||
size="sm"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={e => onValueChange?.(e.target.value)}
|
||||
startAdornment={<Search size={16} className="command-input-icon" />}
|
||||
className="command-input"
|
||||
{...props}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
CommandInput.displayName = 'CommandInput'
|
||||
|
||||
export { CommandDialog, CommandInput }
|
||||
@@ -1,79 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Typography,
|
||||
} from '@/fakemui'
|
||||
|
||||
// CommandItem
|
||||
interface CommandItemProps {
|
||||
children: ReactNode
|
||||
value?: string
|
||||
onSelect?: (value: string) => void
|
||||
disabled?: boolean
|
||||
icon?: ReactNode
|
||||
shortcut?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CommandItem = forwardRef<HTMLLIElement, CommandItemProps>(
|
||||
({ children, value, onSelect, disabled, icon, shortcut, className, ...props }, ref) => {
|
||||
return (
|
||||
<ListItem ref={ref} disablePadding className={className} {...props}>
|
||||
<ListItemButton
|
||||
disabled={disabled}
|
||||
onClick={() => onSelect?.(value || '')}
|
||||
className="command-item-button"
|
||||
>
|
||||
{icon && <ListItemIcon className="command-item-icon">{icon}</ListItemIcon>}
|
||||
<ListItemText primary={children} />
|
||||
{shortcut && (
|
||||
<Typography variant="caption" className="command-item-shortcut">
|
||||
{shortcut}
|
||||
</Typography>
|
||||
)}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
)
|
||||
CommandItem.displayName = 'CommandItem'
|
||||
|
||||
// CommandSeparator
|
||||
const CommandSeparator = forwardRef<HTMLHRElement, { className?: string }>((props, ref) => {
|
||||
return <Divider ref={ref} className="command-separator" {...props} />
|
||||
})
|
||||
CommandSeparator.displayName = 'CommandSeparator'
|
||||
|
||||
// CommandShortcut
|
||||
const CommandShortcut = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<Typography variant="caption" className="command-shortcut">
|
||||
{children}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = 'CommandShortcut'
|
||||
|
||||
// CommandLoading
|
||||
const CommandLoading = ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<Box className="command-loading">
|
||||
<CircularProgress size={16} />
|
||||
<Typography variant="body2">
|
||||
{children || 'Loading...'}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
CommandLoading.displayName = 'CommandLoading'
|
||||
|
||||
export { CommandItem, CommandLoading, CommandSeparator, CommandShortcut }
|
||||
@@ -1,64 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { Box, List, Typography } from '@/fakemui'
|
||||
|
||||
// CommandList
|
||||
interface CommandListProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CommandList = forwardRef<HTMLDivElement, CommandListProps>(({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`command-list ${className || ''}`} {...props}>
|
||||
<List dense disablePadding>
|
||||
{children}
|
||||
</List>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
CommandList.displayName = 'CommandList'
|
||||
|
||||
// CommandEmpty
|
||||
interface CommandEmptyProps {
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CommandEmpty = forwardRef<HTMLDivElement, CommandEmptyProps>(
|
||||
({ children = 'No results found.', className, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`command-empty ${className || ''}`} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
CommandEmpty.displayName = 'CommandEmpty'
|
||||
|
||||
// CommandGroup
|
||||
interface CommandGroupProps {
|
||||
children: ReactNode
|
||||
heading?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CommandGroup = forwardRef<HTMLDivElement, CommandGroupProps>(
|
||||
({ children, heading, className, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} className={`command-group ${className || ''}`} {...props}>
|
||||
{heading && (
|
||||
<Typography variant="overline" className="command-group-heading">
|
||||
{heading}
|
||||
</Typography>
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
CommandGroup.displayName = 'CommandGroup'
|
||||
|
||||
export { CommandEmpty, CommandGroup, CommandList }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user