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:
2026-02-01 22:37:35 +00:00
parent 156715f36d
commit 8fcc71d530
21 changed files with 275 additions and 1961 deletions

View File

@@ -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)
// =============================================================================

View File

@@ -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": [

View 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;
}
`

View File

@@ -58,3 +58,10 @@ export {
type AvatarSkeletonProps,
type TextSkeletonProps,
} from './skeleton'
// Access denied component
export {
AccessDenied,
accessDeniedStyles,
type AccessDeniedProps,
} from './access-denied'

View File

@@ -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",

View File

@@ -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'

View File

@@ -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>
)
}

View File

@@ -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}
/>
)
}

View File

@@ -10,7 +10,7 @@ import {
ErrorState,
NoConnectionState,
LoadingCompleteState,
} from './EmptyState'
} from '@metabuilder/components'
/**
* EmptyStateShowcase - Demonstrates all empty state variants

View File

@@ -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
}

View File

@@ -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}</>
}

View File

@@ -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

View File

@@ -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>
)
}

View File

@@ -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'

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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.

View File

@@ -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'

View File

@@ -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'
}

View File

@@ -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'