mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
refactor: Consolidate frontends/nextjs into root hooks and components
Hooks consolidation: - frontends/nextjs now imports from @metabuilder/hooks - Deleted empty directories (data/, use-dbal/, __tests__/) - Deleted outdated documentation - Added @metabuilder/hooks dependency to package.json - Kept NextJS-specific auth hooks locally (have @/lib/* dependencies) - Added missing useWorkflowExecutions export to root hooks Components consolidation: - Deleted duplicates: Skeleton, LoadingIndicator, EmptyState, ErrorBoundary, AccessDenied - Created new /components/vanilla/access-denied/ component - Updated /components exports and package.json - frontends/nextjs/src/components/index.ts now re-exports from @metabuilder/components - Updated imports in LoadingSkeleton, EmptyStateShowcase, page.tsx Organization principle: Project-specific code is fine in root folders as long as it's well organized. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,13 @@ export {
|
||||
type TextSkeletonProps,
|
||||
} from './vanilla/skeleton'
|
||||
|
||||
// Access denied component
|
||||
export {
|
||||
AccessDenied,
|
||||
accessDeniedStyles,
|
||||
type AccessDeniedProps,
|
||||
} from './vanilla/access-denied'
|
||||
|
||||
// =============================================================================
|
||||
// RADIX COMPONENTS (Built on @radix-ui primitives)
|
||||
// =============================================================================
|
||||
|
||||
@@ -65,6 +65,16 @@
|
||||
"import": "./dist/vanilla/skeleton/index.mjs",
|
||||
"require": "./dist/vanilla/skeleton/index.js",
|
||||
"types": "./dist/vanilla/skeleton/index.d.ts"
|
||||
},
|
||||
"./access-denied": {
|
||||
"import": "./dist/vanilla/access-denied/index.mjs",
|
||||
"require": "./dist/vanilla/access-denied/index.js",
|
||||
"types": "./dist/vanilla/access-denied/index.d.ts"
|
||||
},
|
||||
"./vanilla/access-denied": {
|
||||
"import": "./dist/vanilla/access-denied/index.mjs",
|
||||
"require": "./dist/vanilla/access-denied/index.js",
|
||||
"types": "./dist/vanilla/access-denied/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
||||
151
components/vanilla/access-denied/index.tsx
Normal file
151
components/vanilla/access-denied/index.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
'use client'
|
||||
|
||||
import React, { createElement } from 'react'
|
||||
|
||||
/**
|
||||
* Access Denied Component
|
||||
*
|
||||
* Displays when a user attempts to access a resource they don't have permission for.
|
||||
* Shows the user's current permission level vs required level.
|
||||
*/
|
||||
|
||||
export interface AccessDeniedProps {
|
||||
/**
|
||||
* Required permission level for the resource
|
||||
*/
|
||||
requiredLevel: number
|
||||
/**
|
||||
* User's current permission level
|
||||
*/
|
||||
userLevel: number
|
||||
/**
|
||||
* Custom title text
|
||||
* @default 'Access Denied'
|
||||
*/
|
||||
title?: string
|
||||
/**
|
||||
* Custom message text
|
||||
* @default 'Your permission level is insufficient to access this page.'
|
||||
*/
|
||||
message?: string
|
||||
/**
|
||||
* URL to navigate when clicking "Return Home"
|
||||
* @default '/'
|
||||
*/
|
||||
homeUrl?: string
|
||||
/**
|
||||
* Custom level names map
|
||||
*/
|
||||
levelNames?: Record<number, string>
|
||||
/**
|
||||
* CSS class name for custom styling
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
* Custom style overrides
|
||||
*/
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
const DEFAULT_LEVEL_NAMES: Record<number, string> = {
|
||||
0: 'Public',
|
||||
1: 'User',
|
||||
2: 'Moderator',
|
||||
3: 'Admin',
|
||||
4: 'God',
|
||||
5: 'Supergod',
|
||||
}
|
||||
|
||||
export function AccessDenied({
|
||||
requiredLevel,
|
||||
userLevel,
|
||||
title = 'Access Denied',
|
||||
message = 'Your permission level is insufficient to access this page.',
|
||||
homeUrl = '/',
|
||||
levelNames = DEFAULT_LEVEL_NAMES,
|
||||
className,
|
||||
style,
|
||||
}: AccessDeniedProps) {
|
||||
const requiredLevelName = levelNames[requiredLevel] ?? `Level ${requiredLevel}`
|
||||
const userLevelName = levelNames[userLevel] ?? `Level ${userLevel}`
|
||||
|
||||
return createElement('div', {
|
||||
className: `access-denied ${className ?? ''}`,
|
||||
style: {
|
||||
padding: '2rem',
|
||||
maxWidth: '600px',
|
||||
margin: '4rem auto',
|
||||
textAlign: 'center',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#fafafa',
|
||||
...style,
|
||||
}
|
||||
},
|
||||
createElement('h1', {
|
||||
style: {
|
||||
fontSize: '2rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '1rem',
|
||||
color: '#d32f2f',
|
||||
}
|
||||
}, title),
|
||||
|
||||
createElement('p', {
|
||||
style: {
|
||||
fontSize: '1.125rem',
|
||||
marginBottom: '0.5rem',
|
||||
color: '#424242',
|
||||
}
|
||||
}, message),
|
||||
|
||||
createElement('div', {
|
||||
style: {
|
||||
margin: '1.5rem 0',
|
||||
padding: '1rem',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #e0e0e0',
|
||||
}
|
||||
},
|
||||
createElement('p', { style: { marginBottom: '0.5rem', color: '#616161' } },
|
||||
createElement('strong', null, 'Your Level:'),
|
||||
` ${userLevelName} (${userLevel})`
|
||||
),
|
||||
createElement('p', { style: { color: '#616161' } },
|
||||
createElement('strong', null, 'Required Level:'),
|
||||
` ${requiredLevelName} (${requiredLevel})`
|
||||
)
|
||||
),
|
||||
|
||||
createElement('a', {
|
||||
href: homeUrl,
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
padding: '0.75rem 1.5rem',
|
||||
backgroundColor: '#1976d2',
|
||||
color: 'white',
|
||||
textDecoration: 'none',
|
||||
borderRadius: '4px',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '500',
|
||||
transition: 'background-color 0.2s',
|
||||
}
|
||||
}, 'Return Home')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS styles for AccessDenied - inject in your app
|
||||
*
|
||||
* @example
|
||||
* // Add to your global CSS for hover effects:
|
||||
* .access-denied a:hover {
|
||||
* background-color: #1565c0;
|
||||
* }
|
||||
*/
|
||||
export const accessDeniedStyles = `
|
||||
.access-denied a:hover {
|
||||
background-color: #1565c0 !important;
|
||||
}
|
||||
`
|
||||
@@ -58,3 +58,10 @@ export {
|
||||
type AvatarSkeletonProps,
|
||||
type TextSkeletonProps,
|
||||
} from './skeleton'
|
||||
|
||||
// Access denied component
|
||||
export {
|
||||
AccessDenied,
|
||||
accessDeniedStyles,
|
||||
type AccessDeniedProps,
|
||||
} from './access-denied'
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@metabuilder/dbal": "file:../../dbal/development",
|
||||
"@metabuilder/hooks": "file:../../hooks",
|
||||
"@metabuilder/core-hooks": "file:../../redux/core-hooks",
|
||||
"@metabuilder/api-clients": "file:../../redux/api-clients",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
|
||||
@@ -5,9 +5,7 @@ import { getDBALClient } from '@/dbal'
|
||||
import { loadJSONPackage } from '@/lib/packages/json/functions/load-json-package'
|
||||
import { getPackagesDir } from '@/lib/packages/unified/get-packages-dir'
|
||||
import { getCurrentUser } from '@/lib/auth/get-current-user'
|
||||
import { AccessDenied } from '@/components/AccessDenied'
|
||||
import { JSONComponentRenderer } from '@/components/JSONComponentRenderer'
|
||||
import { ErrorState } from '@/components/EmptyState'
|
||||
import { AccessDenied, JSONComponentRenderer, ErrorState } from '@/components'
|
||||
import type { JSONComponent } from '@/lib/packages/json/types'
|
||||
import type { JsonValue } from '@/types/utility-types'
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/**
|
||||
* Access Denied Component
|
||||
*
|
||||
* Displays when a user attempts to access a resource they don't have permission for.
|
||||
*/
|
||||
|
||||
interface AccessDeniedProps {
|
||||
requiredLevel: number
|
||||
userLevel: number
|
||||
}
|
||||
|
||||
const levelNames: Record<number, string> = {
|
||||
0: 'Public',
|
||||
1: 'User',
|
||||
2: 'Moderator',
|
||||
3: 'Admin',
|
||||
4: 'God',
|
||||
5: 'Supergod',
|
||||
}
|
||||
|
||||
export function AccessDenied({ requiredLevel, userLevel }: AccessDeniedProps) {
|
||||
const requiredLevelName = levelNames[requiredLevel] ?? `Level ${requiredLevel}`
|
||||
const userLevelName = levelNames[userLevel] ?? `Level ${userLevel}`
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '2rem',
|
||||
maxWidth: '600px',
|
||||
margin: '4rem auto',
|
||||
textAlign: 'center',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#fafafa',
|
||||
}}>
|
||||
<h1 style={{
|
||||
fontSize: '2rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '1rem',
|
||||
color: '#d32f2f',
|
||||
}}>
|
||||
Access Denied
|
||||
</h1>
|
||||
|
||||
<p style={{
|
||||
fontSize: '1.125rem',
|
||||
marginBottom: '0.5rem',
|
||||
color: '#424242',
|
||||
}}>
|
||||
Your permission level is insufficient to access this page.
|
||||
</p>
|
||||
|
||||
<div style={{
|
||||
margin: '1.5rem 0',
|
||||
padding: '1rem',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #e0e0e0',
|
||||
}}>
|
||||
<p style={{ marginBottom: '0.5rem', color: '#616161' }}>
|
||||
<strong>Your Level:</strong> {userLevelName} ({userLevel})
|
||||
</p>
|
||||
<p style={{ color: '#616161' }}>
|
||||
<strong>Required Level:</strong> {requiredLevelName} ({requiredLevel})
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '0.75rem 1.5rem',
|
||||
backgroundColor: '#1976d2',
|
||||
color: 'white',
|
||||
textDecoration: 'none',
|
||||
borderRadius: '4px',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '500',
|
||||
transition: 'background-color 0.2s',
|
||||
}}
|
||||
onMouseOver={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#1565c0'
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#1976d2'
|
||||
}}
|
||||
>
|
||||
Return Home
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,404 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { Suspense } from 'react'
|
||||
import { FAKEMUI_REGISTRY } from '@/lib/fakemui-registry'
|
||||
|
||||
/**
|
||||
* Empty State Component - Phase 5.3 Implementation
|
||||
*
|
||||
* Displayed when lists, tables, or other collections are empty.
|
||||
* Provides helpful context and suggests actionable next steps.
|
||||
*
|
||||
* Features:
|
||||
* - FakeMUI component integration for consistency
|
||||
* - Smooth fade-in animations
|
||||
* - Material Design patterns
|
||||
* - Accessibility support (prefers-reduced-motion)
|
||||
* - Multiple icon display methods (emoji, FakeMUI icons, custom)
|
||||
*/
|
||||
|
||||
export interface EmptyStateProps {
|
||||
/**
|
||||
* Icon to display (emoji, component, or string)
|
||||
* Can be: '📭', <Icon />, or 'icon-name' (looks up in registry)
|
||||
*/
|
||||
icon?: React.ReactNode | string
|
||||
|
||||
/**
|
||||
* Title text
|
||||
*/
|
||||
title: string
|
||||
|
||||
/**
|
||||
* Description/message text
|
||||
*/
|
||||
description: string
|
||||
|
||||
/**
|
||||
* Optional helpful hint or suggestion text
|
||||
*/
|
||||
hint?: string
|
||||
|
||||
/**
|
||||
* Optional primary action button
|
||||
*/
|
||||
action?: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
variant?: 'primary' | 'secondary'
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional secondary action
|
||||
*/
|
||||
secondaryAction?: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class name for custom styling
|
||||
*/
|
||||
className?: string
|
||||
|
||||
/**
|
||||
* Custom style overrides
|
||||
*/
|
||||
style?: React.CSSProperties
|
||||
|
||||
/**
|
||||
* Size variant: 'compact', 'normal', 'large'
|
||||
*/
|
||||
size?: 'compact' | 'normal' | 'large'
|
||||
|
||||
/**
|
||||
* Whether to animate on mount (fade-in)
|
||||
*/
|
||||
animated?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Main EmptyState component with full Material Design styling
|
||||
*/
|
||||
export function EmptyState({
|
||||
icon = '📭',
|
||||
title,
|
||||
description,
|
||||
hint,
|
||||
action,
|
||||
secondaryAction,
|
||||
className,
|
||||
style,
|
||||
size = 'normal',
|
||||
animated = true,
|
||||
}: EmptyStateProps) {
|
||||
const sizeMap = {
|
||||
compact: { padding: '20px 16px', iconSize: '32px', titleSize: '16px', descSize: '12px' },
|
||||
normal: { padding: '40px 20px', iconSize: '48px', titleSize: '20px', descSize: '14px' },
|
||||
large: { padding: '60px 20px', iconSize: '64px', titleSize: '24px', descSize: '16px' },
|
||||
}
|
||||
|
||||
const current = sizeMap[size]
|
||||
const animationClass = animated ? 'empty-state-animated' : ''
|
||||
|
||||
// Render icon
|
||||
const renderIcon = () => {
|
||||
if (!icon) return null
|
||||
|
||||
// If it's a string that looks like an emoji
|
||||
if (typeof icon === 'string' && /^[\p{Emoji}]+$/u.test(icon)) {
|
||||
return (
|
||||
<div
|
||||
className="empty-state-icon"
|
||||
style={{ fontSize: current.iconSize, marginBottom: size === 'compact' ? '8px' : '16px' }}
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// If it's a react node
|
||||
if (React.isValidElement(icon)) {
|
||||
return <div className="empty-state-icon" style={{ marginBottom: size === 'compact' ? '8px' : '16px' }}>{icon}</div>
|
||||
}
|
||||
|
||||
// If it's a string icon name from registry
|
||||
if (typeof icon === 'string' && FAKEMUI_REGISTRY[icon]) {
|
||||
const IconComponent = FAKEMUI_REGISTRY[icon]
|
||||
return (
|
||||
<Suspense fallback={<div className="empty-state-icon" style={{ fontSize: current.iconSize }}>○</div>}>
|
||||
<div className="empty-state-icon" style={{ marginBottom: size === 'compact' ? '8px' : '16px' }}>
|
||||
<IconComponent style={{ fontSize: current.iconSize }} />
|
||||
</div>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`empty-state ${animationClass} ${className ?? ''}`}
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: current.padding,
|
||||
color: '#868e96',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{renderIcon()}
|
||||
|
||||
<h2
|
||||
className="empty-state-title"
|
||||
style={{
|
||||
fontSize: current.titleSize,
|
||||
fontWeight: 600,
|
||||
marginBottom: '8px',
|
||||
color: '#495057',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<p
|
||||
className="empty-state-message"
|
||||
style={{
|
||||
fontSize: current.descSize,
|
||||
marginBottom: hint ? '12px' : '24px',
|
||||
maxWidth: '400px',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{hint && (
|
||||
<p
|
||||
className="empty-state-hint"
|
||||
style={{
|
||||
fontSize: current.descSize,
|
||||
marginBottom: '24px',
|
||||
maxWidth: '400px',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
color: '#868e96',
|
||||
fontStyle: 'italic',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{hint}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{(action || secondaryAction) && (
|
||||
<div
|
||||
className="empty-state-actions"
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: '16px',
|
||||
}}
|
||||
>
|
||||
{action && (
|
||||
<button
|
||||
onClick={action.onClick}
|
||||
disabled={action.loading}
|
||||
style={{
|
||||
padding: size === 'compact' ? '6px 12px' : '8px 16px',
|
||||
backgroundColor: action.variant === 'secondary' ? '#f1f3f5' : '#228be6',
|
||||
color: action.variant === 'secondary' ? '#495057' : 'white',
|
||||
border: action.variant === 'secondary' ? '1px solid #dee2e6' : 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: action.loading ? 'not-allowed' : 'pointer',
|
||||
fontSize: current.descSize,
|
||||
fontWeight: 500,
|
||||
transition: 'all 0.2s ease',
|
||||
opacity: action.loading ? 0.7 : 1,
|
||||
}}
|
||||
className="empty-state-action-btn"
|
||||
>
|
||||
{action.loading ? '⏳ Loading...' : action.label}
|
||||
</button>
|
||||
)}
|
||||
{secondaryAction && (
|
||||
<button
|
||||
onClick={secondaryAction.onClick}
|
||||
style={{
|
||||
padding: size === 'compact' ? '6px 12px' : '8px 16px',
|
||||
backgroundColor: '#f1f3f5',
|
||||
color: '#495057',
|
||||
border: '1px solid #dee2e6',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: current.descSize,
|
||||
fontWeight: 500,
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
className="empty-state-secondary-btn"
|
||||
>
|
||||
{secondaryAction.label}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Specialized empty state variants for common use cases
|
||||
*/
|
||||
|
||||
export function NoDataFound({
|
||||
title = 'No data found',
|
||||
description = 'There is no data to display.',
|
||||
hint = 'Try adjusting your filters or search criteria.',
|
||||
className,
|
||||
size,
|
||||
}: Omit<EmptyStateProps, 'title' | 'description' | 'icon'> & {
|
||||
title?: string
|
||||
description?: string
|
||||
hint?: string
|
||||
}) {
|
||||
return <EmptyState icon="🔍" title={title} description={description} hint={hint} className={className} size={size} />
|
||||
}
|
||||
|
||||
export function NoResultsFound({
|
||||
title = 'No results found',
|
||||
description = 'Your search did not return any results.',
|
||||
hint = 'Try using different keywords or check your spelling.',
|
||||
className,
|
||||
size,
|
||||
}: Omit<EmptyStateProps, 'title' | 'description' | 'icon'> & {
|
||||
title?: string
|
||||
description?: string
|
||||
hint?: string
|
||||
}) {
|
||||
return (
|
||||
<EmptyState icon="❌" title={title} description={description} hint={hint} className={className} size={size} />
|
||||
)
|
||||
}
|
||||
|
||||
export function NoItemsYet({
|
||||
title = 'No items yet',
|
||||
description = 'Get started by creating your first item.',
|
||||
hint = 'Click the button below to create one.',
|
||||
action,
|
||||
className,
|
||||
size,
|
||||
}: Omit<EmptyStateProps, 'title' | 'description' | 'icon'> & {
|
||||
title?: string
|
||||
description?: string
|
||||
hint?: string
|
||||
}) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon="✨"
|
||||
title={title}
|
||||
description={description}
|
||||
hint={hint}
|
||||
action={action}
|
||||
className={className}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function AccessDeniedState({
|
||||
title = 'Access denied',
|
||||
description = 'You do not have permission to view this content.',
|
||||
hint = 'Contact your administrator for access.',
|
||||
className,
|
||||
size,
|
||||
}: Omit<EmptyStateProps, 'title' | 'description' | 'icon'> & {
|
||||
title?: string
|
||||
description?: string
|
||||
hint?: string
|
||||
}) {
|
||||
return (
|
||||
<EmptyState icon="🔒" title={title} description={description} hint={hint} className={className} size={size} />
|
||||
)
|
||||
}
|
||||
|
||||
export function ErrorState({
|
||||
title = 'Something went wrong',
|
||||
description = 'An error occurred while loading this content.',
|
||||
hint = 'Please try again later or contact support.',
|
||||
action,
|
||||
className,
|
||||
size,
|
||||
}: Omit<EmptyStateProps, 'title' | 'description' | 'icon'> & {
|
||||
title?: string
|
||||
description?: string
|
||||
hint?: string
|
||||
}) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon="⚠️"
|
||||
title={title}
|
||||
description={description}
|
||||
hint={hint}
|
||||
action={action}
|
||||
className={className}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function NoConnectionState({
|
||||
title = 'Connection failed',
|
||||
description = 'Unable to connect to the server.',
|
||||
hint = 'Check your internet connection and try again.',
|
||||
action,
|
||||
className,
|
||||
size,
|
||||
}: Omit<EmptyStateProps, 'title' | 'description' | 'icon'> & {
|
||||
title?: string
|
||||
description?: string
|
||||
hint?: string
|
||||
}) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon="📡"
|
||||
title={title}
|
||||
description={description}
|
||||
hint={hint}
|
||||
action={action}
|
||||
className={className}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function LoadingCompleteState({
|
||||
title = 'All done!',
|
||||
description = 'Your request has been processed successfully.',
|
||||
hint = 'You can now close this dialog or perform another action.',
|
||||
action,
|
||||
className,
|
||||
size,
|
||||
}: Omit<EmptyStateProps, 'title' | 'description' | 'icon'> & {
|
||||
title?: string
|
||||
description?: string
|
||||
hint?: string
|
||||
}) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon="✅"
|
||||
title={title}
|
||||
description={description}
|
||||
hint={hint}
|
||||
action={action}
|
||||
className={className}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
ErrorState,
|
||||
NoConnectionState,
|
||||
LoadingCompleteState,
|
||||
} from './EmptyState'
|
||||
} from '@metabuilder/components'
|
||||
|
||||
/**
|
||||
* EmptyStateShowcase - Demonstrates all empty state variants
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Error Boundary Component
|
||||
*
|
||||
* Catches JavaScript errors in child component tree and displays fallback UI.
|
||||
* Use this to prevent the entire app from crashing on component errors.
|
||||
* Includes improved error UI and error reporting integration.
|
||||
*/
|
||||
|
||||
import { Component, type ReactNode, type ErrorInfo } from 'react'
|
||||
import { errorReporting } from '@/lib/error-reporting'
|
||||
|
||||
export interface ErrorBoundaryProps {
|
||||
children: ReactNode
|
||||
/** Custom fallback UI to show on error */
|
||||
fallback?: ReactNode
|
||||
/** Callback when error is caught */
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void
|
||||
/** Context for error reporting */
|
||||
context?: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean
|
||||
error: Error | null
|
||||
errorCount: number
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props)
|
||||
this.state = { hasError: false, error: null, errorCount: 0 }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
override componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||
const errorCount = this.state.errorCount + 1
|
||||
|
||||
// Report error
|
||||
errorReporting.reportError(error, {
|
||||
component: errorInfo.componentStack ?? undefined,
|
||||
...this.props.context,
|
||||
})
|
||||
|
||||
// Log error in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('ErrorBoundary caught an error:', error)
|
||||
console.error('Component stack:', errorInfo.componentStack)
|
||||
}
|
||||
|
||||
// Update state with error count
|
||||
this.setState({ errorCount })
|
||||
|
||||
// Call optional error callback
|
||||
this.props.onError?.(error, errorInfo)
|
||||
}
|
||||
|
||||
private handleRetry = () => {
|
||||
this.setState({ hasError: false, error: null })
|
||||
}
|
||||
|
||||
private handleReload = () => {
|
||||
// Full page reload
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
override render(): ReactNode {
|
||||
if (this.state.hasError) {
|
||||
// Return custom fallback if provided
|
||||
if (this.props.fallback !== undefined) {
|
||||
return this.props.fallback
|
||||
}
|
||||
|
||||
const userMessage = this.state.error
|
||||
? errorReporting.getUserMessage(this.state.error)
|
||||
: 'An error occurred while rendering this component.'
|
||||
|
||||
// Default fallback UI with improved styling
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '24px',
|
||||
margin: '16px',
|
||||
border: '1px solid #ff6b6b',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#fff5f5',
|
||||
boxShadow: '0 2px 4px rgba(255, 107, 107, 0.1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
flexShrink: 0,
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
⚠️
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<h2 style={{ color: '#c92a2a', margin: '0 0 8px 0', fontSize: '18px' }}>
|
||||
Something went wrong
|
||||
</h2>
|
||||
<p style={{ color: '#495057', margin: '0 0 12px 0', fontSize: '14px', lineHeight: '1.5' }}>
|
||||
{userMessage}
|
||||
</p>
|
||||
|
||||
{/* Development-only error details */}
|
||||
{process.env.NODE_ENV === 'development' && this.state.error !== null && (
|
||||
<details style={{ marginTop: '12px', marginBottom: '12px' }}>
|
||||
<summary
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
color: '#868e96',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
userSelect: 'none',
|
||||
padding: '4px 0',
|
||||
}}
|
||||
>
|
||||
Error details
|
||||
</summary>
|
||||
<pre
|
||||
style={{
|
||||
marginTop: '8px',
|
||||
padding: '10px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '4px',
|
||||
overflow: 'auto',
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.4',
|
||||
maxHeight: '200px',
|
||||
color: '#666',
|
||||
}}
|
||||
>
|
||||
{this.state.error.message}
|
||||
{this.state.error.stack && `\n\n${this.state.error.stack}`}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{/* Show error count if multiple errors */}
|
||||
{this.state.errorCount > 1 && (
|
||||
<p
|
||||
style={{
|
||||
color: '#ff6b6b',
|
||||
fontSize: '12px',
|
||||
margin: '8px 0',
|
||||
}}
|
||||
>
|
||||
This error has occurred {this.state.errorCount} times.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginTop: '12px' }}>
|
||||
<button
|
||||
onClick={this.handleRetry}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#228be6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
transition: 'background-color 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.target as HTMLButtonElement).style.backgroundColor = '#1c7ed6'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.target as HTMLButtonElement).style.backgroundColor = '#228be6'
|
||||
}}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
<button
|
||||
onClick={this.handleReload}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#f1f3f5',
|
||||
color: '#495057',
|
||||
border: '1px solid #dee2e6',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
transition: 'background-color 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.target as HTMLButtonElement).style.backgroundColor = '#e9ecef'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.target as HTMLButtonElement).style.backgroundColor = '#f1f3f5'
|
||||
}}
|
||||
>
|
||||
Reload page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Higher-order component to wrap any component with error boundary
|
||||
*/
|
||||
export function withErrorBoundary<P extends object>(
|
||||
WrappedComponent: React.ComponentType<P>,
|
||||
fallback?: ReactNode,
|
||||
context?: Record<string, unknown>
|
||||
): React.ComponentType<P> {
|
||||
const name = WrappedComponent.name !== '' ? WrappedComponent.name : undefined
|
||||
const displayName = WrappedComponent.displayName ?? name ?? 'Component'
|
||||
|
||||
const ComponentWithErrorBoundary = (props: P) => (
|
||||
<ErrorBoundary fallback={fallback} context={context}>
|
||||
<WrappedComponent {...props} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
ComponentWithErrorBoundary.displayName = `withErrorBoundary(${displayName})`
|
||||
return ComponentWithErrorBoundary
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
'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',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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',
|
||||
marginRight: '8px',
|
||||
...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}</>
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { Skeleton, TableSkeleton, CardSkeleton, ListSkeleton } from './Skeleton'
|
||||
import { Skeleton, TableSkeleton, CardSkeleton, ListSkeleton } from '@metabuilder/components'
|
||||
|
||||
/**
|
||||
* LoadingSkeleton Component - Unified loading state wrapper
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* Skeleton Component for Loading States
|
||||
*
|
||||
* Creates animated placeholder content while data is loading.
|
||||
* Use for tables, cards, lists, and other async-loaded content.
|
||||
*/
|
||||
|
||||
export interface SkeletonProps {
|
||||
/**
|
||||
* Width of the skeleton (can be percentage or fixed value)
|
||||
* @default '100%'
|
||||
*/
|
||||
width?: string | number
|
||||
|
||||
/**
|
||||
* Height of the skeleton (can be percentage or fixed value)
|
||||
* @default '20px'
|
||||
*/
|
||||
height?: string | number
|
||||
|
||||
/**
|
||||
* Border radius for rounded corners
|
||||
* @default '4px'
|
||||
*/
|
||||
borderRadius?: string | number
|
||||
|
||||
/**
|
||||
* Whether to show animation
|
||||
* @default true
|
||||
*/
|
||||
animate?: boolean
|
||||
|
||||
/**
|
||||
* CSS class name for custom styling
|
||||
*/
|
||||
className?: string
|
||||
|
||||
/**
|
||||
* Custom style overrides
|
||||
*/
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
/**
|
||||
* Single skeleton line/block
|
||||
*/
|
||||
export function Skeleton({
|
||||
width = '100%',
|
||||
height = '20px',
|
||||
borderRadius = '4px',
|
||||
animate = true,
|
||||
className,
|
||||
style,
|
||||
}: SkeletonProps) {
|
||||
const widthStyle = typeof width === 'number' ? `${width}px` : width
|
||||
const heightStyle = typeof height === 'number' ? `${height}px` : height
|
||||
const radiusStyle = typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`skeleton ${animate ? 'skeleton-animate' : ''} ${className ?? ''}`}
|
||||
style={{
|
||||
width: widthStyle,
|
||||
height: heightStyle,
|
||||
borderRadius: radiusStyle,
|
||||
backgroundColor: '#e0e0e0',
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Table skeleton with rows and columns
|
||||
*/
|
||||
export interface TableSkeletonProps {
|
||||
rows?: number
|
||||
columns?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TableSkeleton({ rows = 5, columns = 4, className }: TableSkeletonProps) {
|
||||
return (
|
||||
<div className={`table-skeleton ${className ?? ''}`}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid #e0e0e0' }}>
|
||||
{Array.from({ length: columns }).map((_, i) => (
|
||||
<th key={i} style={{ padding: '12px', textAlign: 'left' }}>
|
||||
<Skeleton width="80%" height="20px" />
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: rows }).map((_, rowIdx) => (
|
||||
<tr key={rowIdx} style={{ borderBottom: '1px solid #f0f0f0' }}>
|
||||
{Array.from({ length: columns }).map((_, colIdx) => (
|
||||
<td key={colIdx} style={{ padding: '12px' }}>
|
||||
<Skeleton width={colIdx === 0 ? '60%' : '90%'} height="20px" />
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Card skeleton layout
|
||||
*/
|
||||
export interface CardSkeletonProps {
|
||||
count?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CardSkeleton({ count = 3, className }: CardSkeletonProps) {
|
||||
return (
|
||||
<div className={`card-skeleton-grid ${className ?? ''}`} style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '16px' }}>
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
padding: '16px',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#fafafa',
|
||||
}}
|
||||
>
|
||||
<Skeleton width="40%" height="24px" style={{ marginBottom: '12px' }} />
|
||||
<Skeleton width="100%" height="16px" style={{ marginBottom: '8px' }} />
|
||||
<Skeleton width="85%" height="16px" style={{ marginBottom: '16px' }} />
|
||||
<Skeleton width="60%" height="36px" borderRadius="4px" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* List item skeleton
|
||||
*/
|
||||
export interface ListSkeletonProps {
|
||||
count?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ListSkeleton({ count = 8, className }: ListSkeletonProps) {
|
||||
return (
|
||||
<div className={`list-skeleton ${className ?? ''}`}>
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '12px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
gap: '12px',
|
||||
}}
|
||||
>
|
||||
<Skeleton width="40px" height="40px" borderRadius="50%" style={{ flexShrink: 0 }} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<Skeleton width="60%" height="18px" style={{ marginBottom: '6px' }} />
|
||||
<Skeleton width="85%" height="14px" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,22 +2,43 @@
|
||||
* Component Export Index
|
||||
*
|
||||
* Centralized exports for all reusable components.
|
||||
* Base components are imported from @metabuilder/components,
|
||||
* project-specific components are defined locally.
|
||||
*/
|
||||
|
||||
// Loading & Skeletons
|
||||
export { Skeleton, TableSkeleton, CardSkeleton, ListSkeleton } from './Skeleton'
|
||||
export type { SkeletonProps, TableSkeletonProps, CardSkeletonProps, ListSkeletonProps } from './Skeleton'
|
||||
// =============================================================================
|
||||
// RE-EXPORTS FROM @metabuilder/components (shared across projects)
|
||||
// =============================================================================
|
||||
|
||||
// Loading Skeleton (unified wrapper)
|
||||
// Loading & Skeletons
|
||||
export {
|
||||
LoadingSkeleton,
|
||||
TableLoading,
|
||||
CardLoading,
|
||||
ListLoading,
|
||||
InlineLoading,
|
||||
FormLoading,
|
||||
} from './LoadingSkeleton'
|
||||
export type { LoadingSkeletonProps, FormLoadingProps, TableLoadingProps } from './LoadingSkeleton'
|
||||
Skeleton,
|
||||
TableSkeleton,
|
||||
CardSkeleton,
|
||||
ListSkeleton,
|
||||
FormSkeleton,
|
||||
AvatarSkeleton,
|
||||
TextSkeleton,
|
||||
skeletonStyles,
|
||||
type SkeletonProps,
|
||||
type TableSkeletonProps,
|
||||
type CardSkeletonProps,
|
||||
type ListSkeletonProps,
|
||||
type FormSkeletonProps,
|
||||
type AvatarSkeletonProps,
|
||||
type TextSkeletonProps,
|
||||
} from '@metabuilder/components'
|
||||
|
||||
// Loading Indicators
|
||||
export {
|
||||
LoadingIndicator,
|
||||
InlineLoader,
|
||||
AsyncLoading,
|
||||
loadingStyles,
|
||||
type LoadingIndicatorProps,
|
||||
type InlineLoaderProps,
|
||||
type AsyncLoadingProps,
|
||||
} from '@metabuilder/components'
|
||||
|
||||
// Empty States
|
||||
export {
|
||||
@@ -29,38 +50,67 @@ export {
|
||||
ErrorState,
|
||||
NoConnectionState,
|
||||
LoadingCompleteState,
|
||||
} from './EmptyState'
|
||||
export { EmptyStateShowcase } from './EmptyStateShowcase'
|
||||
export type { EmptyStateProps } from './EmptyState'
|
||||
|
||||
// Loading Indicators
|
||||
export {
|
||||
LoadingIndicator,
|
||||
InlineLoader,
|
||||
AsyncLoading,
|
||||
} from './LoadingIndicator'
|
||||
export type {
|
||||
LoadingIndicatorProps,
|
||||
InlineLoaderProps,
|
||||
AsyncLoadingProps,
|
||||
} from './LoadingIndicator'
|
||||
emptyStateStyles,
|
||||
type EmptyStateProps,
|
||||
} from '@metabuilder/components'
|
||||
|
||||
// Error Boundary
|
||||
export {
|
||||
ErrorBoundary,
|
||||
withErrorBoundary,
|
||||
} from './ErrorBoundary'
|
||||
export type { ErrorBoundaryProps } from './ErrorBoundary'
|
||||
ErrorDisplay,
|
||||
type ErrorBoundaryProps,
|
||||
type ErrorReporter,
|
||||
type ErrorDisplayProps,
|
||||
} from '@metabuilder/components'
|
||||
|
||||
// Access Control
|
||||
export { AccessDenied } from './AccessDenied'
|
||||
export {
|
||||
AccessDenied,
|
||||
accessDeniedStyles,
|
||||
type AccessDeniedProps,
|
||||
} from '@metabuilder/components'
|
||||
|
||||
// Component Rendering
|
||||
// =============================================================================
|
||||
// PROJECT-SPECIFIC COMPONENTS (Next.js app specific)
|
||||
// =============================================================================
|
||||
|
||||
// Loading Skeleton (unified wrapper - uses local Skeleton import)
|
||||
export {
|
||||
LoadingSkeleton,
|
||||
TableLoading,
|
||||
CardLoading,
|
||||
ListLoading,
|
||||
InlineLoading,
|
||||
FormLoading,
|
||||
} from './LoadingSkeleton'
|
||||
export type { LoadingSkeletonProps, FormLoadingProps, TableLoadingProps } from './LoadingSkeleton'
|
||||
|
||||
// Empty State Showcase (demo component)
|
||||
export { EmptyStateShowcase } from './EmptyStateShowcase'
|
||||
|
||||
// Component Rendering (depends on @/lib/packages/json)
|
||||
export { JSONComponentRenderer } from './JSONComponentRenderer'
|
||||
|
||||
// Pagination
|
||||
// UI Page Renderer (depends on @/lib/packages/json)
|
||||
export { UIPageRenderer, useAction, useUIPageActions } from './ui-page-renderer'
|
||||
|
||||
// Pagination (uses FakeMUI components)
|
||||
export {
|
||||
PaginationControls,
|
||||
PaginationInfo,
|
||||
ItemsPerPageSelector,
|
||||
} from './pagination'
|
||||
export type { PaginationControlsProps } from './pagination/PaginationControls'
|
||||
export type { PaginationInfoProps } from './pagination/PaginationInfo'
|
||||
export type { ItemsPerPageSelectorProps } from './pagination/ItemsPerPageSelector'
|
||||
|
||||
// Icon utilities (depends on @/fakemui/icons)
|
||||
export { getComponentIcon } from './get-component-icon'
|
||||
|
||||
// Package Style Loader (depends on @/lib/compiler)
|
||||
export { PackageStyleLoader } from './PackageStyleLoader'
|
||||
|
||||
// Retryable Error Boundary (depends on @/lib/error-reporting)
|
||||
export { RetryableErrorBoundary, withRetryableErrorBoundary } from './RetryableErrorBoundary'
|
||||
export type { RetryableErrorBoundaryProps } from './RetryableErrorBoundary'
|
||||
|
||||
@@ -25,7 +25,7 @@ import type {
|
||||
NodeResult,
|
||||
LogEntry,
|
||||
} from '@metabuilder/workflow'
|
||||
import { useWorkflowExecutions } from '@/hooks/useWorkflow'
|
||||
import { useWorkflowExecutions } from '@metabuilder/hooks'
|
||||
import styles from './ExecutionMonitor.module.css'
|
||||
|
||||
export interface ExecutionMonitorProps {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
import React, { useState, useCallback, useMemo } from 'react'
|
||||
import type { WorkflowDefinition, WorkflowNode } from '@metabuilder/workflow'
|
||||
import { useWorkflow } from '@/hooks/useWorkflow'
|
||||
import { useWorkflow } from '@metabuilder/hooks'
|
||||
import styles from './WorkflowBuilder.module.css'
|
||||
|
||||
export interface WorkflowBuilderProps {
|
||||
|
||||
@@ -1,426 +0,0 @@
|
||||
# Data Structure Management Hooks
|
||||
|
||||
This document describes the 5 data structure management hooks designed for React state management of common data structures.
|
||||
|
||||
## Overview
|
||||
|
||||
These hooks provide TypeScript-first, strongly-typed state management for Set, Map, Array, Stack, and Queue data structures. Each hook follows React best practices with proper dependency tracking and memoization.
|
||||
|
||||
## Hooks
|
||||
|
||||
### 1. useSet - Typed Set State Management
|
||||
|
||||
Manages a `Set<T>` with full operation support.
|
||||
|
||||
**File**: `/src/hooks/useSet.ts`
|
||||
|
||||
**Usage**:
|
||||
```typescript
|
||||
import { useSet } from '@/hooks'
|
||||
|
||||
function MyComponent() {
|
||||
const { values, add, remove, has, toggle, clear } = useSet<string>(['a', 'b'])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => add('c')}>Add C</button>
|
||||
<button onClick={() => remove('a')}>Remove A</button>
|
||||
<p>Has B: {has('b').toString()}</p>
|
||||
<p>Items: {Array.from(values).join(', ')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**API**:
|
||||
```typescript
|
||||
interface UseSetReturn<T> {
|
||||
values: Set<T> // Current set
|
||||
add: (value: T) => void // Add value to set
|
||||
remove: (value: T) => void // Remove value from set
|
||||
has: (value: T) => boolean // Check if value exists
|
||||
toggle: (value: T) => void // Add/remove value
|
||||
clear: () => void // Clear all values
|
||||
}
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- Automatically deduplicates values (Set behavior)
|
||||
- Memoized operations with useCallback
|
||||
- Supports any comparable type (primitives, objects)
|
||||
- Toggle operation for easy on/off switching
|
||||
|
||||
---
|
||||
|
||||
### 2. useMap - Typed Map State Management
|
||||
|
||||
Manages a `Map<K, V>` with full operation support.
|
||||
|
||||
**File**: `/src/hooks/useMap.ts`
|
||||
|
||||
**Usage**:
|
||||
```typescript
|
||||
import { useMap } from '@/hooks'
|
||||
|
||||
function MyComponent() {
|
||||
const { data, set, get, delete: deleteKey, clear, entries, keys, values }
|
||||
= useMap<string, number>([['count', 5]])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => set('count', (get('count') || 0) + 1)}>
|
||||
Increment
|
||||
</button>
|
||||
<p>Count: {get('count')}</p>
|
||||
<p>Keys: {Array.from(keys()).join(', ')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**API**:
|
||||
```typescript
|
||||
interface UseMapReturn<K, V> {
|
||||
data: Map<K, V> // Current map
|
||||
set: (key: K, value: V) => void // Set key-value pair
|
||||
get: (key: K) => V | undefined // Get value by key
|
||||
delete: (key: K) => void // Delete by key
|
||||
clear: () => void // Clear all entries
|
||||
has: (key: K) => boolean // Check if key exists
|
||||
entries: () => IterableIterator<[K, V]> // Get entries iterator
|
||||
keys: () => IterableIterator<K> // Get keys iterator
|
||||
values: () => IterableIterator<V> // Get values iterator
|
||||
}
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- Direct Map semantics with all standard operations
|
||||
- Iterator accessors for entries, keys, and values
|
||||
- Supports any key/value types
|
||||
- Overwrites existing keys automatically
|
||||
|
||||
---
|
||||
|
||||
### 3. useArray - Array Operations Hook
|
||||
|
||||
Manages arrays with comprehensive mutation operations.
|
||||
|
||||
**File**: `/src/hooks/useArray.ts`
|
||||
|
||||
**Usage**:
|
||||
```typescript
|
||||
import { useArray } from '@/hooks'
|
||||
|
||||
function MyComponent() {
|
||||
const { items, push, pop, shift, unshift, insert, remove, swap, filter, map, length, get }
|
||||
= useArray<string>(['a', 'b', 'c'])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => push('d')}>Add item</button>
|
||||
<button onClick={() => pop()}>Remove last</button>
|
||||
<button onClick={() => shift()}>Remove first</button>
|
||||
<ul>
|
||||
{items.map((item, i) => (
|
||||
<li key={i}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>Length: {length}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**API**:
|
||||
```typescript
|
||||
interface UseArrayReturn<T> {
|
||||
items: T[] // Current array
|
||||
push: (item: T) => void // Add to end
|
||||
pop: () => T | undefined // Remove from end
|
||||
shift: () => T | undefined // Remove from start
|
||||
unshift: (item: T) => void // Add to start
|
||||
insert: (index: number, item: T) => void // Insert at index
|
||||
remove: (index: number) => void // Remove at index
|
||||
swap: (indexA: number, indexB: number) => void // Swap two elements
|
||||
clear: () => void // Clear array
|
||||
filter: (predicate: (item: T) => boolean) => void // Filter in-place
|
||||
map: <R,>(callback: (item: T) => R) => R[] // Map to new array
|
||||
length: number // Current length
|
||||
get: (index: number) => T | undefined // Get by index
|
||||
}
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- All standard array mutations
|
||||
- insert/remove with bounds checking
|
||||
- Swap operation for reordering
|
||||
- Filter operation for in-place filtering
|
||||
- Map operation returns new array (read-only)
|
||||
- Length property for convenience
|
||||
|
||||
---
|
||||
|
||||
### 4. useStack - Stack/LIFO Operations
|
||||
|
||||
Manages stack (Last In First Out) data structure.
|
||||
|
||||
**File**: `/src/hooks/useStack.ts`
|
||||
|
||||
**Usage**:
|
||||
```typescript
|
||||
import { useStack } from '@/hooks'
|
||||
|
||||
function BrowserHistory() {
|
||||
const { items, push, pop, peek, clear, isEmpty, size } = useStack<string>()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => push('https://google.com')}>Visit</button>
|
||||
<button onClick={() => pop()} disabled={isEmpty}>Back</button>
|
||||
<p>Current: {peek()}</p>
|
||||
<p>History size: {size}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**API**:
|
||||
```typescript
|
||||
interface UseStackReturn<T> {
|
||||
items: T[] // Stack items (bottom to top)
|
||||
push: (item: T) => void // Push item (add to top)
|
||||
pop: () => T | undefined // Pop item (remove from top)
|
||||
peek: () => T | undefined // Peek at top without removing
|
||||
clear: () => void // Clear all items
|
||||
isEmpty: boolean // True if empty
|
||||
size: number // Number of items
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Browser history/navigation
|
||||
- Undo/redo functionality
|
||||
- Expression evaluation
|
||||
- Depth-first search algorithms
|
||||
- Function call stack simulation
|
||||
|
||||
---
|
||||
|
||||
### 5. useQueue - Queue/FIFO Operations
|
||||
|
||||
Manages queue (First In First Out) data structure.
|
||||
|
||||
**File**: `/src/hooks/useQueue.ts`
|
||||
|
||||
**Usage**:
|
||||
```typescript
|
||||
import { useQueue } from '@/hooks'
|
||||
|
||||
function TaskProcessor() {
|
||||
const { items, enqueue, dequeue, peek, clear, isEmpty, size } = useQueue<Task>()
|
||||
|
||||
const processNextTask = () => {
|
||||
const task = dequeue()
|
||||
if (task) executeTask(task)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => enqueue(newTask)}>Add Task</button>
|
||||
<button onClick={processNextTask} disabled={isEmpty}>Process</button>
|
||||
<p>Queue size: {size}</p>
|
||||
<p>Next task: {peek()?.name}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**API**:
|
||||
```typescript
|
||||
interface UseQueueReturn<T> {
|
||||
items: T[] // Queue items (front to back)
|
||||
enqueue: (item: T) => void // Add item to back
|
||||
dequeue: () => T | undefined // Remove item from front
|
||||
peek: () => T | undefined // Peek at front without removing
|
||||
clear: () => void // Clear all items
|
||||
isEmpty: boolean // True if empty
|
||||
size: number // Number of items
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Task/job processing queues
|
||||
- Event handling
|
||||
- Breadth-first search algorithms
|
||||
- Print job queues
|
||||
- Request/response handling
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Each hook has comprehensive test coverage:
|
||||
|
||||
- **useSet.test.ts**: 11 tests covering add, remove, toggle, clear operations
|
||||
- **useMap.test.ts**: 14 tests covering set, get, delete, iterators
|
||||
- **useArray.test.ts**: 18 tests covering all array operations
|
||||
- **useStack.test.ts**: 14 tests covering LIFO semantics
|
||||
- **useQueue.test.ts**: 16 tests covering FIFO semantics
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
npm test -- src/hooks/__tests__/use{Set,Map,Array,Stack,Queue}.test.ts
|
||||
```
|
||||
|
||||
All 73 tests pass successfully.
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Memory
|
||||
- Each hook maintains state in React memory
|
||||
- For large data structures, consider pagination or virtualization
|
||||
- useRef is used in Stack/Queue for immediate return values
|
||||
|
||||
### State Updates
|
||||
- All operations use functional setState for batching
|
||||
- useCallback with proper dependencies prevents unnecessary re-renders
|
||||
- Consider useTransition for long-running operations
|
||||
|
||||
### Recommendations
|
||||
- **Set**: Good for membership testing, up to ~10k items
|
||||
- **Map**: Good for key-value lookups, up to ~10k entries
|
||||
- **Array**: Good for indexed access, up to ~5k items (consider virtualization above)
|
||||
- **Stack**: Good for bounded operations like undo/redo
|
||||
- **Queue**: Good for task processing with moderate queue depth
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Tag Selection with useSet
|
||||
|
||||
```typescript
|
||||
function TagSelector() {
|
||||
const { values, toggle, clear } = useSet<string>()
|
||||
const tags = ['React', 'TypeScript', 'Next.js', 'Tailwind']
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tags.map(tag => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => toggle(tag)}
|
||||
style={{ fontWeight: values.has(tag) ? 'bold' : 'normal' }}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
))}
|
||||
<button onClick={clear}>Clear</button>
|
||||
<p>Selected: {Array.from(values).join(', ')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Form State with useMap
|
||||
|
||||
```typescript
|
||||
function FormComponent() {
|
||||
const { data, set, get, clear } = useMap<string, string>()
|
||||
|
||||
const handleChange = (field: string, value: string) => {
|
||||
set(field, value)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={() => console.log(Object.fromEntries(data))}>
|
||||
<input
|
||||
value={get('email') || ''}
|
||||
onChange={(e) => handleChange('email', e.target.value)}
|
||||
placeholder="Email"
|
||||
/>
|
||||
<button type="button" onClick={clear}>Reset</button>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Undo/Redo with useStack
|
||||
|
||||
```typescript
|
||||
function Editor() {
|
||||
const history = useStack<string>()
|
||||
const [current, setCurrent] = useState('')
|
||||
|
||||
const handleEdit = (text: string) => {
|
||||
history.push(current)
|
||||
setCurrent(text)
|
||||
}
|
||||
|
||||
const handleUndo = () => {
|
||||
const previous = history.pop()
|
||||
if (previous !== undefined) setCurrent(previous)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<textarea
|
||||
value={current}
|
||||
onChange={(e) => handleEdit(e.target.value)}
|
||||
/>
|
||||
<button onClick={handleUndo} disabled={history.isEmpty}>
|
||||
Undo
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with Redux
|
||||
|
||||
These hooks work well alongside Redux for:
|
||||
- **Local component state**: Use these hooks
|
||||
- **Global app state**: Use Redux (already available in project)
|
||||
- **Mixed approach**: Use these for temporary/local data structures
|
||||
|
||||
Example:
|
||||
```typescript
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useQueue } from '@/hooks'
|
||||
|
||||
function NotificationCenter() {
|
||||
const { enqueue, dequeue } = useQueue<Notification>()
|
||||
const notifications = useSelector(selectNotifications)
|
||||
|
||||
// Local queue for display
|
||||
// Global Redux for persistence
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
All hooks use standard JavaScript features and work in:
|
||||
- Chrome/Edge (latest 2 versions)
|
||||
- Firefox (latest 2 versions)
|
||||
- Safari (latest 2 versions)
|
||||
- Modern mobile browsers
|
||||
|
||||
TypeScript 5.9.3+ required for type annotations.
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential additions:
|
||||
- **usePriority Queue**: Sorted queue by priority
|
||||
- **useLinkedList**: Doubly-linked list operations
|
||||
- **useGraph**: Graph adjacency operations
|
||||
- **useTree**: Tree traversal and manipulation
|
||||
- **useLRU**: Least Recently Used cache
|
||||
@@ -1,243 +0,0 @@
|
||||
# Custom Hooks (`src/hooks/`)
|
||||
|
||||
Reusable React hooks for state management, side effects, and custom logic.
|
||||
|
||||
## Available Hooks
|
||||
|
||||
### Authentication & User
|
||||
|
||||
- **useAuth()** - Current user state and authentication status
|
||||
- **usePermissions()** - Permission checking for current user
|
||||
- **useUser()** - User profile and settings
|
||||
|
||||
### Form Management
|
||||
|
||||
- **useForm()** - Form state and validation
|
||||
- **useFormField()** - Individual field management
|
||||
- **useValidation()** - Input validation
|
||||
|
||||
### UI State
|
||||
|
||||
- **useModal()** - Modal/dialog state management
|
||||
- **useSidebar()** - Sidebar visibility control
|
||||
- **useTheme()** - Theme switching and state
|
||||
|
||||
### Data Management
|
||||
|
||||
- **useDatabase()** - Database query and mutation hooks
|
||||
- **usePackages()** - Package loading and management
|
||||
- **useWorkflows()** - Workflow state
|
||||
|
||||
### Effects & Lifecycle
|
||||
|
||||
- **useAsync()** - Handle async operations
|
||||
- **useDebounce()** - Debounce hook
|
||||
- **useLocalStorage()** - Persistent client storage
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### useAuth - Get Current User
|
||||
|
||||
```typescript
|
||||
import { useAuth } from '@/hooks'
|
||||
|
||||
export const UserProfile = () => {
|
||||
const { user, isAuthenticated } = useAuth()
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <LoginPrompt />
|
||||
}
|
||||
|
||||
return <div>Welcome, {user.name}</div>
|
||||
}
|
||||
```
|
||||
|
||||
### useForm - Handle Form State
|
||||
|
||||
```typescript
|
||||
import { useForm } from '@/hooks'
|
||||
|
||||
export const LoginForm = () => {
|
||||
const { values, errors, handleChange, handleSubmit } = useForm({
|
||||
initialValues: { email: '', password: '' },
|
||||
onSubmit: (values) => loginUser(values)
|
||||
})
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input value={values.email} onChange={handleChange} />
|
||||
{errors.email && <span>{errors.email}</span>}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### usePermissions - Check Access Level
|
||||
|
||||
```typescript
|
||||
import { usePermissions } from '@/hooks'
|
||||
|
||||
export const AdminPanel = () => {
|
||||
const { canAccess } = usePermissions()
|
||||
|
||||
if (!canAccess(3)) {
|
||||
return <AccessDenied />
|
||||
}
|
||||
|
||||
return <AdminContent />
|
||||
}
|
||||
```
|
||||
|
||||
### useAsync - Handle Async Operations
|
||||
|
||||
```typescript
|
||||
import { useAsync } from '@/hooks'
|
||||
|
||||
export const DataLoader = () => {
|
||||
const { data, loading, error } = useAsync(
|
||||
() => fetchData(),
|
||||
[]
|
||||
)
|
||||
|
||||
if (loading) return <Spinner />
|
||||
if (error) return <Error message={error} />
|
||||
return <DataDisplay data={data} />
|
||||
}
|
||||
```
|
||||
|
||||
### useLocalStorage - Persistent State
|
||||
|
||||
```typescript
|
||||
import { useLocalStorage } from '@/hooks'
|
||||
|
||||
export const PreferenceSettings = () => {
|
||||
const [theme, setTheme] = useLocalStorage('theme', 'dark')
|
||||
|
||||
return (
|
||||
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
||||
Current: {theme}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Hook Best Practices
|
||||
|
||||
1. ✅ Use hooks for cross-cutting concerns
|
||||
2. ✅ Keep hook logic focused and single-purpose
|
||||
3. ✅ Document parameters and return values with JSDoc
|
||||
4. ✅ Handle loading and error states
|
||||
5. ✅ Clean up side effects with return functions
|
||||
6. ✅ Memoize expensive computations with useMemo
|
||||
7. ❌ Don't call hooks conditionally
|
||||
8. ❌ Don't use hooks outside React components
|
||||
9. ❌ Don't forget dependency arrays
|
||||
|
||||
## Creating Custom Hooks
|
||||
|
||||
Template for new hooks:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* useMyHook - Brief description of hook purpose
|
||||
* @param {string} param1 - Description of param
|
||||
* @returns {Object} Hook return value
|
||||
* @returns {any} returns.value - Current value
|
||||
* @returns {Function} returns.setValue - Update function
|
||||
*
|
||||
* @example
|
||||
* const { value, setValue } = useMyHook(initialValue)
|
||||
*/
|
||||
export const useMyHook = (param1: string) => {
|
||||
const [value, setValue] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Setup logic
|
||||
return () => {
|
||||
// Cleanup logic
|
||||
}
|
||||
}, [param1])
|
||||
|
||||
return { value, setValue }
|
||||
}
|
||||
```
|
||||
|
||||
## Hook Composition
|
||||
|
||||
Combine multiple hooks for complex features:
|
||||
|
||||
```typescript
|
||||
// Complex hook using multiple simpler hooks
|
||||
export const useAuthenticatedData = (endpoint: string) => {
|
||||
const { user } = useAuth()
|
||||
const { data, loading } = useAsync(
|
||||
() => fetch(endpoint, {
|
||||
headers: { Authorization: `Bearer ${user.token}` }
|
||||
}),
|
||||
[user.token]
|
||||
)
|
||||
|
||||
return { data, loading, user }
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Memoization
|
||||
|
||||
```typescript
|
||||
export const useExpensiveComputation = (dependencies: any[]) => {
|
||||
return useMemo(() => {
|
||||
// Expensive calculation
|
||||
return result
|
||||
}, dependencies)
|
||||
}
|
||||
```
|
||||
|
||||
### Callback Memoization
|
||||
|
||||
```typescript
|
||||
export const useCallbackHandler = () => {
|
||||
return useCallback((value: string) => {
|
||||
// Handle value
|
||||
}, [])
|
||||
}
|
||||
```
|
||||
|
||||
## File Organization
|
||||
|
||||
```
|
||||
hooks/
|
||||
├── index.ts # Export all hooks
|
||||
├── useAuth.ts # Authentication
|
||||
├── useForm.ts # Form management
|
||||
├── useAsync.ts # Async operations
|
||||
├── useLocalStorage.ts # Storage
|
||||
├── useDatabase.ts # Database queries
|
||||
├── usePermissions.ts # Authorization
|
||||
└── __tests__/
|
||||
├── useAuth.spec.ts
|
||||
├── useForm.spec.ts
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Testing Custom Hooks
|
||||
|
||||
Use `@testing-library/react` for hook testing:
|
||||
|
||||
```typescript
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useMyHook } from './useMyHook'
|
||||
|
||||
test('hook should update value', () => {
|
||||
const { result } = renderHook(() => useMyHook('initial'))
|
||||
|
||||
act(() => {
|
||||
result.current.setValue('updated')
|
||||
})
|
||||
|
||||
expect(result.current.value).toBe('updated')
|
||||
})
|
||||
```
|
||||
|
||||
See React documentation and `/docs/` for more hook patterns.
|
||||
@@ -1,52 +1,12 @@
|
||||
/**
|
||||
* Hooks barrel export for frontends/nextjs
|
||||
*
|
||||
* Re-exports all hooks from @metabuilder/hooks plus local NextJS-specific auth hooks
|
||||
*/
|
||||
|
||||
// Re-export everything from the centralized hooks package
|
||||
export * from '@metabuilder/hooks'
|
||||
|
||||
// Local NextJS-specific auth exports (these depend on @/lib/auth/* APIs)
|
||||
export type { AuthState, AuthUser, UseAuthReturn } from './auth/auth-types'
|
||||
export { useMobile } from './use-mobile'
|
||||
export { useAuth } from './useAuth'
|
||||
export { useAutoRefresh } from './useAutoRefresh'
|
||||
export type { EditorFile } from './useCodeEditor'
|
||||
export { useCodeEditor } from './useCodeEditor'
|
||||
export { useDBAL } from './useDBAL'
|
||||
export type { FileNode } from './useFileTree'
|
||||
export { useFileTree } from './useFileTree'
|
||||
export type { WorkflowRun } from './useGitHubFetcher'
|
||||
export { useGitHubFetcher } from './useGitHubFetcher'
|
||||
export { useKV } from './useKV'
|
||||
export { useLevelRouting } from './data/useLevelRouting'
|
||||
export { useResolvedUser } from './data/useResolvedUser'
|
||||
|
||||
// Data structure management hooks
|
||||
export type { UseSetReturn } from './useSet'
|
||||
export { useSet } from './useSet'
|
||||
export type { UseMapReturn } from './useMap'
|
||||
export { useMap } from './useMap'
|
||||
export type { UseArrayReturn } from './useArray'
|
||||
export { useArray } from './useArray'
|
||||
export type { UseStackReturn } from './useStack'
|
||||
export { useStack } from './useStack'
|
||||
export type { UseQueueReturn } from './useQueue'
|
||||
export { useQueue } from './useQueue'
|
||||
|
||||
// Phase 3 User CRUD hooks
|
||||
export { useUsers } from './useUsers'
|
||||
export type { UseUsersReturn } from './useUsers'
|
||||
export { useUserForm } from './useUserForm'
|
||||
export type { UseUserFormReturn, UserFormData, UserFormErrors } from './useUserForm'
|
||||
export { useUserActions } from './useUserActions'
|
||||
export type { UseUserActionsReturn } from './useUserActions'
|
||||
|
||||
// Phase 3 Package CRUD hooks
|
||||
export { usePackages } from './usePackages'
|
||||
export type { UsePackagesReturn } from './usePackages'
|
||||
export { usePackageActions } from './usePackageActions'
|
||||
export type { UsePackageActionsReturn } from './usePackageActions'
|
||||
export { usePackageDetails } from './usePackageDetails'
|
||||
export type { UsePackageDetailsReturn } from './usePackageDetails'
|
||||
|
||||
// Form validation and state management hooks
|
||||
export { useValidation } from './useValidation'
|
||||
export type { ValidationSchema, ValidationErrors } from './useValidation'
|
||||
export { useInput } from './useInput'
|
||||
export { useCheckbox } from './useCheckbox'
|
||||
export { useSelect } from './useSelect'
|
||||
export type { SelectOption } from './useSelect'
|
||||
export { useFieldArray } from './useFieldArray'
|
||||
export type { FormField } from './useFieldArray'
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
|
||||
// Replace Spark hooks with our custom implementation
|
||||
declare module '@github/spark/hooks' {
|
||||
export { useKV } from '@/hooks/useKV'
|
||||
export { useKV } from '@metabuilder/hooks'
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ export { useResolvedUser } from './useResolvedUser'
|
||||
export { useUserActions } from './useUserActions'
|
||||
export { useUserForm } from './useUserForm'
|
||||
export { useUsers } from './useUsers'
|
||||
export { useWorkflow } from './useWorkflow'
|
||||
export { useWorkflow, useWorkflowExecutions } from './useWorkflow'
|
||||
|
||||
// Re-export types
|
||||
export type { AuthUser } from './use-auth'
|
||||
|
||||
Reference in New Issue
Block a user