From 8fcc71d5305f5bef880db26dde3736d24faaa7fb Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 1 Feb 2026 22:37:35 +0000 Subject: [PATCH] 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 --- components/index.tsx | 7 + components/package.json | 10 + components/vanilla/access-denied/index.tsx | 151 +++++++ components/vanilla/index.ts | 7 + frontends/nextjs/package.json | 1 + frontends/nextjs/src/app/page.tsx | 4 +- .../nextjs/src/components/AccessDenied.tsx | 91 ---- .../nextjs/src/components/EmptyState.tsx | 404 ----------------- .../src/components/EmptyStateShowcase.tsx | 2 +- .../nextjs/src/components/ErrorBoundary.tsx | 235 ---------- .../src/components/LoadingIndicator.tsx | 294 ------------ .../nextjs/src/components/LoadingSkeleton.tsx | 2 +- frontends/nextjs/src/components/Skeleton.tsx | 177 -------- frontends/nextjs/src/components/index.ts | 114 +++-- .../components/workflow/ExecutionMonitor.tsx | 2 +- .../components/workflow/WorkflowBuilder.tsx | 2 +- .../nextjs/src/hooks/DATA_STRUCTURES_HOOKS.md | 426 ------------------ frontends/nextjs/src/hooks/README.md | 243 ---------- frontends/nextjs/src/hooks/index.ts | 60 +-- .../nextjs/src/types/module-overrides.d.ts | 2 +- hooks/index.ts | 2 +- 21 files changed, 275 insertions(+), 1961 deletions(-) create mode 100644 components/vanilla/access-denied/index.tsx delete mode 100644 frontends/nextjs/src/components/AccessDenied.tsx delete mode 100644 frontends/nextjs/src/components/EmptyState.tsx delete mode 100644 frontends/nextjs/src/components/ErrorBoundary.tsx delete mode 100644 frontends/nextjs/src/components/LoadingIndicator.tsx delete mode 100644 frontends/nextjs/src/components/Skeleton.tsx delete mode 100644 frontends/nextjs/src/hooks/DATA_STRUCTURES_HOOKS.md delete mode 100644 frontends/nextjs/src/hooks/README.md diff --git a/components/index.tsx b/components/index.tsx index 3158e7a6c..a238f6d08 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -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) // ============================================================================= diff --git a/components/package.json b/components/package.json index 20bd50cd5..d641b1bc8 100644 --- a/components/package.json +++ b/components/package.json @@ -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": [ diff --git a/components/vanilla/access-denied/index.tsx b/components/vanilla/access-denied/index.tsx new file mode 100644 index 000000000..77ff602e1 --- /dev/null +++ b/components/vanilla/access-denied/index.tsx @@ -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 + /** + * CSS class name for custom styling + */ + className?: string + /** + * Custom style overrides + */ + style?: React.CSSProperties +} + +const DEFAULT_LEVEL_NAMES: Record = { + 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; +} +` diff --git a/components/vanilla/index.ts b/components/vanilla/index.ts index 2d0016b1b..7019ae395 100644 --- a/components/vanilla/index.ts +++ b/components/vanilla/index.ts @@ -58,3 +58,10 @@ export { type AvatarSkeletonProps, type TextSkeletonProps, } from './skeleton' + +// Access denied component +export { + AccessDenied, + accessDeniedStyles, + type AccessDeniedProps, +} from './access-denied' diff --git a/frontends/nextjs/package.json b/frontends/nextjs/package.json index be1e6f7ff..a520de63d 100644 --- a/frontends/nextjs/package.json +++ b/frontends/nextjs/package.json @@ -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", diff --git a/frontends/nextjs/src/app/page.tsx b/frontends/nextjs/src/app/page.tsx index 4130ce4f7..13241759a 100644 --- a/frontends/nextjs/src/app/page.tsx +++ b/frontends/nextjs/src/app/page.tsx @@ -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' diff --git a/frontends/nextjs/src/components/AccessDenied.tsx b/frontends/nextjs/src/components/AccessDenied.tsx deleted file mode 100644 index 126e9fcbd..000000000 --- a/frontends/nextjs/src/components/AccessDenied.tsx +++ /dev/null @@ -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 = { - 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 ( -
-

- Access Denied -

- -

- Your permission level is insufficient to access this page. -

- -
-

- Your Level: {userLevelName} ({userLevel}) -

-

- Required Level: {requiredLevelName} ({requiredLevel}) -

-
- - { - e.currentTarget.style.backgroundColor = '#1565c0' - }} - onMouseOut={(e) => { - e.currentTarget.style.backgroundColor = '#1976d2' - }} - > - Return Home - -
- ) -} diff --git a/frontends/nextjs/src/components/EmptyState.tsx b/frontends/nextjs/src/components/EmptyState.tsx deleted file mode 100644 index 754937b6d..000000000 --- a/frontends/nextjs/src/components/EmptyState.tsx +++ /dev/null @@ -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: '📭', , 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 ( - - ) - } - - // If it's a react node - if (React.isValidElement(icon)) { - return
{icon}
- } - - // If it's a string icon name from registry - if (typeof icon === 'string' && FAKEMUI_REGISTRY[icon]) { - const IconComponent = FAKEMUI_REGISTRY[icon] - return ( - ○}> -
- -
-
- ) - } - - return null - } - - return ( -
- {renderIcon()} - -

- {title} -

- -

- {description} -

- - {hint && ( -

- {hint} -

- )} - - {(action || secondaryAction) && ( -
- {action && ( - - )} - {secondaryAction && ( - - )} -
- )} -
- ) -} - -/** - * 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 & { - title?: string - description?: string - hint?: string -}) { - return -} - -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 & { - title?: string - description?: string - hint?: string -}) { - return ( - - ) -} - -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 & { - title?: string - description?: string - hint?: string -}) { - return ( - - ) -} - -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 & { - title?: string - description?: string - hint?: string -}) { - return ( - - ) -} - -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 & { - title?: string - description?: string - hint?: string -}) { - return ( - - ) -} - -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 & { - title?: string - description?: string - hint?: string -}) { - return ( - - ) -} - -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 & { - title?: string - description?: string - hint?: string -}) { - return ( - - ) -} diff --git a/frontends/nextjs/src/components/EmptyStateShowcase.tsx b/frontends/nextjs/src/components/EmptyStateShowcase.tsx index bdb99b502..3e71c0d91 100644 --- a/frontends/nextjs/src/components/EmptyStateShowcase.tsx +++ b/frontends/nextjs/src/components/EmptyStateShowcase.tsx @@ -10,7 +10,7 @@ import { ErrorState, NoConnectionState, LoadingCompleteState, -} from './EmptyState' +} from '@metabuilder/components' /** * EmptyStateShowcase - Demonstrates all empty state variants diff --git a/frontends/nextjs/src/components/ErrorBoundary.tsx b/frontends/nextjs/src/components/ErrorBoundary.tsx deleted file mode 100644 index 6d88003e0..000000000 --- a/frontends/nextjs/src/components/ErrorBoundary.tsx +++ /dev/null @@ -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 -} - -interface ErrorBoundaryState { - hasError: boolean - error: Error | null - errorCount: number -} - -export class ErrorBoundary extends Component { - constructor(props: ErrorBoundaryProps) { - super(props) - this.state = { hasError: false, error: null, errorCount: 0 } - } - - static getDerivedStateFromError(error: Error): Partial { - 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 ( -
-
-
- ⚠️ -
-
-

- Something went wrong -

-

- {userMessage} -

- - {/* Development-only error details */} - {process.env.NODE_ENV === 'development' && this.state.error !== null && ( -
- - Error details - -
-                    {this.state.error.message}
-                    {this.state.error.stack && `\n\n${this.state.error.stack}`}
-                  
-
- )} - - {/* Show error count if multiple errors */} - {this.state.errorCount > 1 && ( -

- This error has occurred {this.state.errorCount} times. -

- )} - - {/* Action buttons */} -
- - -
-
-
-
- ) - } - - return this.props.children - } -} - -/** - * Higher-order component to wrap any component with error boundary - */ -export function withErrorBoundary

( - WrappedComponent: React.ComponentType

, - fallback?: ReactNode, - context?: Record -): React.ComponentType

{ - const name = WrappedComponent.name !== '' ? WrappedComponent.name : undefined - const displayName = WrappedComponent.displayName ?? name ?? 'Component' - - const ComponentWithErrorBoundary = (props: P) => ( - - - - ) - - ComponentWithErrorBoundary.displayName = `withErrorBoundary(${displayName})` - return ComponentWithErrorBoundary -} diff --git a/frontends/nextjs/src/components/LoadingIndicator.tsx b/frontends/nextjs/src/components/LoadingIndicator.tsx deleted file mode 100644 index 1a750cf0f..000000000 --- a/frontends/nextjs/src/components/LoadingIndicator.tsx +++ /dev/null @@ -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 ( -

- {variant === 'spinner' && } - {variant === 'bar' && } - {variant === 'dots' && } - {variant === 'pulse' && } - - {message && ( -

- {message} -

- )} -
- ) -} - -/** - * Spinner icon component - */ -interface IconProps { - size: string -} - -function SpinnerIcon({ size }: IconProps) { - return ( -
- ) -} - -function PulseIcon({ size }: IconProps) { - return ( -
- ) -} - -/** - * Progress bar component - */ -interface ProgressBarProps { - size: 'small' | 'medium' | 'large' -} - -function ProgressBar({ size }: ProgressBarProps) { - const heightMap = { - small: '2px', - medium: '4px', - large: '6px', - } - - return ( -
-
-
- ) -} - -/** - * 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 ( -
- {[0, 1, 2].map((i) => ( -
- ))} -
- ) -} - -/** - * 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 ( -
- ) -} - -/** - * 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 ?? - } - - if (error) { - return errorComponent ??
Error loading content
- } - - return <>{children} -} diff --git a/frontends/nextjs/src/components/LoadingSkeleton.tsx b/frontends/nextjs/src/components/LoadingSkeleton.tsx index 7ee1f43c6..c123b00a7 100644 --- a/frontends/nextjs/src/components/LoadingSkeleton.tsx +++ b/frontends/nextjs/src/components/LoadingSkeleton.tsx @@ -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 diff --git a/frontends/nextjs/src/components/Skeleton.tsx b/frontends/nextjs/src/components/Skeleton.tsx deleted file mode 100644 index afd29a647..000000000 --- a/frontends/nextjs/src/components/Skeleton.tsx +++ /dev/null @@ -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 ( -
- ) -} - -/** - * 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 ( -
- - - - {Array.from({ length: columns }).map((_, i) => ( - - ))} - - - - {Array.from({ length: rows }).map((_, rowIdx) => ( - - {Array.from({ length: columns }).map((_, colIdx) => ( - - ))} - - ))} - -
- -
- -
-
- ) -} - -/** - * Card skeleton layout - */ -export interface CardSkeletonProps { - count?: number - className?: string -} - -export function CardSkeleton({ count = 3, className }: CardSkeletonProps) { - return ( -
- {Array.from({ length: count }).map((_, i) => ( -
- - - - -
- ))} -
- ) -} - -/** - * List item skeleton - */ -export interface ListSkeletonProps { - count?: number - className?: string -} - -export function ListSkeleton({ count = 8, className }: ListSkeletonProps) { - return ( -
- {Array.from({ length: count }).map((_, i) => ( -
- -
- - -
-
- ))} -
- ) -} diff --git a/frontends/nextjs/src/components/index.ts b/frontends/nextjs/src/components/index.ts index d1028ddfa..bd8913156 100644 --- a/frontends/nextjs/src/components/index.ts +++ b/frontends/nextjs/src/components/index.ts @@ -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' diff --git a/frontends/nextjs/src/components/workflow/ExecutionMonitor.tsx b/frontends/nextjs/src/components/workflow/ExecutionMonitor.tsx index 7205c2a4a..f346afc07 100644 --- a/frontends/nextjs/src/components/workflow/ExecutionMonitor.tsx +++ b/frontends/nextjs/src/components/workflow/ExecutionMonitor.tsx @@ -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 { diff --git a/frontends/nextjs/src/components/workflow/WorkflowBuilder.tsx b/frontends/nextjs/src/components/workflow/WorkflowBuilder.tsx index 57c70610d..08595e306 100644 --- a/frontends/nextjs/src/components/workflow/WorkflowBuilder.tsx +++ b/frontends/nextjs/src/components/workflow/WorkflowBuilder.tsx @@ -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 { diff --git a/frontends/nextjs/src/hooks/DATA_STRUCTURES_HOOKS.md b/frontends/nextjs/src/hooks/DATA_STRUCTURES_HOOKS.md deleted file mode 100644 index ca558bbea..000000000 --- a/frontends/nextjs/src/hooks/DATA_STRUCTURES_HOOKS.md +++ /dev/null @@ -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` 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(['a', 'b']) - - return ( -
- - -

Has B: {has('b').toString()}

-

Items: {Array.from(values).join(', ')}

-
- ) -} -``` - -**API**: -```typescript -interface UseSetReturn { - values: Set // 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` 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([['count', 5]]) - - return ( -
- -

Count: {get('count')}

-

Keys: {Array.from(keys()).join(', ')}

-
- ) -} -``` - -**API**: -```typescript -interface UseMapReturn { - data: Map // 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 // Get keys iterator - values: () => IterableIterator // 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(['a', 'b', 'c']) - - return ( -
- - - -
    - {items.map((item, i) => ( -
  • {item}
  • - ))} -
-

Length: {length}

-
- ) -} -``` - -**API**: -```typescript -interface UseArrayReturn { - 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: (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() - - return ( -
- - -

Current: {peek()}

-

History size: {size}

-
- ) -} -``` - -**API**: -```typescript -interface UseStackReturn { - 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() - - const processNextTask = () => { - const task = dequeue() - if (task) executeTask(task) - } - - return ( -
- - -

Queue size: {size}

-

Next task: {peek()?.name}

-
- ) -} -``` - -**API**: -```typescript -interface UseQueueReturn { - 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() - const tags = ['React', 'TypeScript', 'Next.js', 'Tailwind'] - - return ( -
- {tags.map(tag => ( - - ))} - -

Selected: {Array.from(values).join(', ')}

-
- ) -} -``` - -### Example 2: Form State with useMap - -```typescript -function FormComponent() { - const { data, set, get, clear } = useMap() - - const handleChange = (field: string, value: string) => { - set(field, value) - } - - return ( -
console.log(Object.fromEntries(data))}> - handleChange('email', e.target.value)} - placeholder="Email" - /> - - -
- ) -} -``` - -### Example 3: Undo/Redo with useStack - -```typescript -function Editor() { - const history = useStack() - 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 ( -
-