Files
johndoe6345789 d23f4a8be4 feat: FakeMUI MUI-compatibility + shared components library + hooks consolidation
FakeMUI Components (MUI API compatibility):
- Add sx prop support to all components via sxToStyle utility
- Add MUI-style variants to Button (contained, outlined)
- Add component prop to Typography for polymorphic rendering
- Add label prop to Chip (MUI uses label vs children)
- Add edge/color/size props to IconButton
- Add component prop to List for nav rendering
- Add href support to ListItemButton
- Add variant prop to Avatar
- Add PaperProps to Drawer

New @metabuilder/components package:
- vanilla/loading - LoadingIndicator, InlineLoader, AsyncLoading
- vanilla/error - ErrorBoundary, ErrorDisplay, withErrorBoundary
- vanilla/empty-state - EmptyState + 7 specialized variants
- vanilla/skeleton - Skeleton + 6 specialized variants
- Organized by framework: vanilla/, radix/, fakemui/

Hooks consolidation (FakeMUI → root hooks/):
- useAccessible (5 accessibility hooks)
- useToast with ToastProvider
- FakeMUI re-exports from hooks for backward compatibility

WorkflowUI fixes:
- Fix showNotification → useUI error/success methods
- Fix Redux reducer setTimeout (Immer proxy issue)
- Fix useRef type error
- Update to Next.js 16.1.6 with webpack mode
- Add @metabuilder/fakemui dependency

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 22:05:47 +00:00

340 lines
6.9 KiB
TypeScript

'use client'
import React from 'react'
/**
* Loading Indicator Component
*
* Shows progress during async operations.
* Supports different display modes: spinner, bar, dots, etc.
*/
export interface LoadingIndicatorProps {
/**
* Whether to show the loading indicator
* @default true
*/
show?: boolean
/**
* Loading message to display
*/
message?: string
/**
* Variant: 'spinner', 'bar', 'dots', 'pulse'
* @default 'spinner'
*/
variant?: 'spinner' | 'bar' | 'dots' | 'pulse'
/**
* Size of the indicator: 'small', 'medium', 'large'
* @default 'medium'
*/
size?: 'small' | 'medium' | 'large'
/**
* Whether to show full page overlay
* @default false
*/
fullPage?: boolean
/**
* CSS class name for custom styling
*/
className?: string
/**
* Custom style overrides
*/
style?: React.CSSProperties
}
export function LoadingIndicator({
show = true,
message,
variant = 'spinner',
size = 'medium',
fullPage = false,
className,
style,
}: LoadingIndicatorProps) {
if (!show) {
return null
}
const sizeMap = {
small: '24px',
medium: '40px',
large: '60px',
}
const containerStyle: React.CSSProperties = fullPage
? {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 9999,
...style,
}
: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '20px',
...style,
}
return (
<div className={`loading-indicator loading-${variant} ${className ?? ''}`} style={containerStyle}>
{variant === 'spinner' && <SpinnerIcon size={sizeMap[size]} />}
{variant === 'bar' && <ProgressBar size={size} />}
{variant === 'dots' && <DotsAnimation size={size} />}
{variant === 'pulse' && <PulseIcon size={sizeMap[size]} />}
{message && (
<p
style={{
marginTop: variant === 'spinner' || variant === 'pulse' ? '16px' : '12px',
color: fullPage ? '#ffffff' : '#495057',
fontSize: size === 'small' ? '12px' : size === 'large' ? '16px' : '14px',
textAlign: 'center',
}}
>
{message}
</p>
)}
</div>
)
}
/**
* Spinner icon component
*/
interface IconProps {
size: string
}
function SpinnerIcon({ size }: IconProps) {
return (
<div
className="loading-spinner"
style={{
width: size,
height: size,
border: '3px solid #e0e0e0',
borderTopColor: '#228be6',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
}}
/>
)
}
function PulseIcon({ size }: IconProps) {
return (
<div
style={{
width: size,
height: size,
borderRadius: '50%',
backgroundColor: '#228be6',
animation: 'pulse-animation 2s ease-in-out infinite',
}}
/>
)
}
/**
* Progress bar component
*/
interface ProgressBarProps {
size: 'small' | 'medium' | 'large'
}
function ProgressBar({ size }: ProgressBarProps) {
const heightMap = {
small: '2px',
medium: '4px',
large: '6px',
}
return (
<div
style={{
width: '200px',
height: heightMap[size],
backgroundColor: '#e0e0e0',
borderRadius: '2px',
overflow: 'hidden',
}}
>
<div
style={{
height: '100%',
backgroundColor: '#228be6',
animation: 'progress-animation 1.5s ease-in-out infinite',
borderRadius: '2px',
}}
/>
</div>
)
}
/**
* Animated dots component
*/
interface DotsAnimationProps {
size: 'small' | 'medium' | 'large'
}
function DotsAnimation({ size }: DotsAnimationProps) {
const dotMap = {
small: '6px',
medium: '10px',
large: '14px',
}
const dotSize = dotMap[size]
return (
<div
style={{
display: 'flex',
gap: '6px',
alignItems: 'center',
}}
>
{[0, 1, 2].map((i) => (
<div
key={i}
style={{
width: dotSize,
height: dotSize,
borderRadius: '50%',
backgroundColor: '#228be6',
animation: `dots-animation 1.4s infinite`,
animationDelay: `${i * 0.16}s`,
}}
/>
))}
</div>
)
}
/**
* Inline loading spinner for buttons and text
*/
export interface InlineLoaderProps {
loading?: boolean
size?: 'small' | 'medium'
style?: React.CSSProperties
}
export function InlineLoader({ loading = true, size = 'small', style }: InlineLoaderProps) {
if (!loading) {
return null
}
const sizeMap = {
small: '16px',
medium: '20px',
}
return (
<div
className="loading-spinner"
style={{
display: 'inline-block',
width: sizeMap[size],
height: sizeMap[size],
border: '2px solid #e0e0e0',
borderTopColor: '#228be6',
borderRadius: '50%',
marginRight: '8px',
animation: 'spin 1s linear infinite',
...style,
}}
/>
)
}
/**
* Loading state for async operations with skeleton fallback
*/
export interface AsyncLoadingProps {
isLoading: boolean
error?: Error | string | null
children: React.ReactNode
skeletonComponent?: React.ReactNode
errorComponent?: React.ReactNode
loadingMessage?: string
}
export function AsyncLoading({
isLoading,
error,
children,
skeletonComponent,
errorComponent,
loadingMessage,
}: AsyncLoadingProps) {
if (isLoading) {
return skeletonComponent ?? <LoadingIndicator show message={loadingMessage} />
}
if (error) {
return errorComponent ?? <div style={{ color: '#c92a2a', padding: '16px' }}>Error loading content</div>
}
return <>{children}</>
}
/**
* CSS keyframes for animations - inject these styles in your app
*
* @example
* // Add to your global CSS:
* @keyframes spin {
* to { transform: rotate(360deg); }
* }
* @keyframes pulse-animation {
* 0%, 100% { opacity: 1; transform: scale(1); }
* 50% { opacity: 0.5; transform: scale(0.8); }
* }
* @keyframes progress-animation {
* 0% { width: 0%; }
* 50% { width: 70%; }
* 100% { width: 100%; }
* }
* @keyframes dots-animation {
* 0%, 80%, 100% { opacity: 0; transform: scale(0); }
* 40% { opacity: 1; transform: scale(1); }
* }
*/
export const loadingStyles = `
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes pulse-animation {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.8); }
}
@keyframes progress-animation {
0% { width: 0%; }
50% { width: 70%; }
100% { width: 100%; }
}
@keyframes dots-animation {
0%, 80%, 100% { opacity: 0; transform: scale(0); }
40% { opacity: 1; transform: scale(1); }
}
`