mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
refactor: Update subprojects to use shared @metabuilder/components
**workflowui**: - Add NotificationAdapter that bridges useUINotifications() to shared NotificationContainer - Update RootLayoutClient to use NotificationAdapter - Mark local NotificationContainer as deprecated **pastebin**: - Update BackendIndicator to use shared BackendStatus component - Keep as thin wrapper that maps getStorageConfig() to status prop - Update tests for new implementation **codegen**: - Update KeyboardShortcutsDialog to use shared KeyboardShortcutsContent - Move hardcoded shortcuts to data array using ShortcutCategory type - Use getPlatformModifier() for cross-platform modifier keys - Keep local Dialog wrapper, use shared content component **components**: - Add tsup.config.ts for building the package - Add package-lock.json All subprojects now depend on @metabuilder/components (file:../components) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@metabuilder/api-clients": "workspace:*",
|
||||
"@metabuilder/components": "workspace:*",
|
||||
"@metabuilder/core-hooks": "workspace:*",
|
||||
"@metabuilder/fakemui": "workspace:*",
|
||||
"@metabuilder/hooks-auth": "workspace:*",
|
||||
|
||||
@@ -5,17 +5,63 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Keyboard } from '@phosphor-icons/react'
|
||||
import {
|
||||
KeyboardShortcutsContent,
|
||||
getPlatformModifier,
|
||||
type ShortcutCategory,
|
||||
} from '@metabuilder/components'
|
||||
|
||||
interface KeyboardShortcutsDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Get codegen-specific keyboard shortcuts
|
||||
* Shortcuts are defined as data for easy maintenance and localization
|
||||
*/
|
||||
function getCodegenShortcuts(): ShortcutCategory[] {
|
||||
const ctrlKey = getPlatformModifier()
|
||||
|
||||
return [
|
||||
{
|
||||
category: 'Navigation',
|
||||
items: [
|
||||
{ keys: [ctrlKey, '1'], description: 'Go to Dashboard' },
|
||||
{ keys: [ctrlKey, '2'], description: 'Go to Code Editor' },
|
||||
{ keys: [ctrlKey, '3'], description: 'Go to Models' },
|
||||
{ keys: [ctrlKey, '4'], description: 'Go to Components' },
|
||||
{ keys: [ctrlKey, '5'], description: 'Go to Component Trees' },
|
||||
{ keys: [ctrlKey, '6'], description: 'Go to Workflows' },
|
||||
{ keys: [ctrlKey, '7'], description: 'Go to Lambdas' },
|
||||
{ keys: [ctrlKey, '8'], description: 'Go to Styling' },
|
||||
{ keys: [ctrlKey, '9'], description: 'Go to Favicon Designer' },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Actions',
|
||||
items: [
|
||||
{ keys: [ctrlKey, 'K'], description: 'Search Everything' },
|
||||
{ keys: [ctrlKey, 'E'], description: 'Export Project' },
|
||||
{ keys: [ctrlKey, 'Shift', 'G'], description: 'AI Generate' },
|
||||
{ keys: [ctrlKey, '/'], description: 'Show Keyboard Shortcuts' },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Code Editor',
|
||||
items: [
|
||||
{ keys: [ctrlKey, 'F'], description: 'Find in file' },
|
||||
{ keys: [ctrlKey, 'H'], description: 'Find and replace' },
|
||||
{ keys: [ctrlKey, 'Z'], description: 'Undo' },
|
||||
{ keys: [ctrlKey, 'Shift', 'Z'], description: 'Redo' },
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function KeyboardShortcutsDialog({ open, onOpenChange }: KeyboardShortcutsDialogProps) {
|
||||
const isMac = navigator.platform.includes('Mac')
|
||||
const ctrlKey = isMac ? '⌘' : 'Ctrl'
|
||||
const shortcuts = getCodegenShortcuts()
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -30,116 +76,12 @@ export function KeyboardShortcutsDialog({ open, onOpenChange }: KeyboardShortcut
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-3">Navigation</h3>
|
||||
<div className="space-y-2">
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, '1']}
|
||||
description="Go to Dashboard"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, '2']}
|
||||
description="Go to Code Editor"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, '3']}
|
||||
description="Go to Models"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, '4']}
|
||||
description="Go to Components"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, '5']}
|
||||
description="Go to Component Trees"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, '6']}
|
||||
description="Go to Workflows"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, '7']}
|
||||
description="Go to Lambdas"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, '8']}
|
||||
description="Go to Styling"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, '9']}
|
||||
description="Go to Favicon Designer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-3">Actions</h3>
|
||||
<div className="space-y-2">
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, 'K']}
|
||||
description="Search Everything"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, 'E']}
|
||||
description="Export Project"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, 'Shift', 'G']}
|
||||
description="AI Generate"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, '/']}
|
||||
description="Show Keyboard Shortcuts"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-3">Code Editor</h3>
|
||||
<div className="space-y-2">
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, 'F']}
|
||||
description="Find in file"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, 'H']}
|
||||
description="Find and replace"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, 'Z']}
|
||||
description="Undo"
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={[ctrlKey, 'Shift', 'Z']}
|
||||
description="Redo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<KeyboardShortcutsContent
|
||||
shortcuts={shortcuts}
|
||||
title=""
|
||||
description=""
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function ShortcutRow({ keys, description }: { keys: string[]; description: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-muted-foreground">{description}</span>
|
||||
<div className="flex gap-1">
|
||||
{keys.map((key, index) => (
|
||||
<kbd
|
||||
key={index}
|
||||
className="px-2 py-1 text-xs font-semibold bg-muted border border-border rounded"
|
||||
>
|
||||
{key}
|
||||
</kbd>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,4 +2,9 @@
|
||||
* KeyboardShortcutsDialogProps - JSON definition interface
|
||||
* Dialog for displaying keyboard shortcuts
|
||||
*/
|
||||
export interface KeyboardShortcutsDialogProps {}
|
||||
export interface KeyboardShortcutsDialogProps {
|
||||
/** Whether the dialog is open */
|
||||
open: boolean
|
||||
/** Callback when open state changes */
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
1544
components/package-lock.json
generated
Normal file
1544
components/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
components/tsup.config.ts
Normal file
22
components/tsup.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
index: 'index.tsx',
|
||||
'vanilla/index': 'vanilla/index.ts',
|
||||
'vanilla/loading/index': 'vanilla/loading/index.tsx',
|
||||
'vanilla/error/index': 'vanilla/error/index.tsx',
|
||||
'vanilla/empty-state/index': 'vanilla/empty-state/index.tsx',
|
||||
'vanilla/skeleton/index': 'vanilla/skeleton/index.tsx',
|
||||
'vanilla/access-denied/index': 'vanilla/access-denied/index.tsx',
|
||||
'vanilla/notifications/index': 'vanilla/notifications/index.tsx',
|
||||
'vanilla/status-indicators/index': 'vanilla/status-indicators/index.tsx',
|
||||
'radix/dialogs/KeyboardShortcutsDialog': 'radix/dialogs/KeyboardShortcutsDialog.tsx',
|
||||
},
|
||||
format: ['cjs', 'esm'],
|
||||
dts: true,
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
external: ['react', 'react-dom'],
|
||||
})
|
||||
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/standalone": "^7.24.0",
|
||||
"@metabuilder/components": "file:../components",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@phosphor-icons/react": "^2.1.7",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
|
||||
@@ -4,6 +4,17 @@ import * as storageModule from '@/lib/storage';
|
||||
|
||||
jest.mock('@/lib/storage');
|
||||
|
||||
// Mock the styles injection to avoid DOM manipulation in tests
|
||||
jest.mock('@metabuilder/components', () => ({
|
||||
BackendStatus: ({ status, showDot }: { status: string; showDot?: boolean }) => (
|
||||
<div data-testid="status-badge" data-status={status} role="status">
|
||||
<span>{status === 'connected' ? 'Connected' : 'Local'}</span>
|
||||
{showDot && <span data-testid="activity-dot" />}
|
||||
</div>
|
||||
),
|
||||
statusIndicatorStyles: '',
|
||||
}));
|
||||
|
||||
describe('BackendIndicator', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -22,37 +33,37 @@ describe('BackendIndicator', () => {
|
||||
expect(screen.getByText('Local')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('has disconnected styling', () => {
|
||||
test('has disconnected status', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('status-badge');
|
||||
expect(indicator).toHaveAttribute('data-status', 'disconnected');
|
||||
});
|
||||
|
||||
test('renders wrapper with test id', () => {
|
||||
render(<BackendIndicator />);
|
||||
const wrapper = screen.getByTestId('backend-indicator');
|
||||
expect(wrapper).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders status badge', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('status-badge');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('has status role for accessibility', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByRole('status');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders with correct content', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('backend-indicator');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders database icon', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('backend-indicator');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('has styling applied', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('backend-indicator');
|
||||
expect(indicator).toHaveClass('flex');
|
||||
});
|
||||
|
||||
test('renders with background and border', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('backend-indicator');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders with proper sizing', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('backend-indicator');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('has tooltip on hover', () => {
|
||||
test('has proper structure', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('backend-indicator');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
@@ -71,27 +82,27 @@ describe('BackendIndicator', () => {
|
||||
expect(screen.getByText('Connected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('has connected styling', () => {
|
||||
test('has connected status', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('backend-indicator');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
const indicator = screen.getByTestId('status-badge');
|
||||
expect(indicator).toHaveAttribute('data-status', 'connected');
|
||||
});
|
||||
|
||||
test('displays connected state', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('backend-indicator');
|
||||
const indicator = screen.getByTestId('status-badge');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders with accent styling', () => {
|
||||
test('renders with connected styling', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('backend-indicator');
|
||||
const indicator = screen.getByTestId('status-badge');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders cloud check icon', () => {
|
||||
test('renders status component', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('backend-indicator');
|
||||
const indicator = screen.getByTestId('status-badge');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -105,8 +116,8 @@ describe('BackendIndicator', () => {
|
||||
|
||||
test('shows dot indicator when auto-configured', () => {
|
||||
(process.env as any).NEXT_PUBLIC_FLASK_BACKEND_URL = 'http://localhost:5000';
|
||||
const { container } = render(<BackendIndicator />);
|
||||
const dot = container.querySelector('[class*="rounded-full"]');
|
||||
render(<BackendIndicator />);
|
||||
const dot = screen.getByTestId('activity-dot');
|
||||
expect(dot).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -117,20 +128,20 @@ describe('BackendIndicator', () => {
|
||||
});
|
||||
render(<BackendIndicator />);
|
||||
expect(screen.getByText('Connected')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('activity-dot')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tooltip functionality', () => {
|
||||
test('renders tooltip for local storage', () => {
|
||||
test('renders status for local storage', () => {
|
||||
(storageModule.getStorageConfig as jest.Mock).mockReturnValue({
|
||||
backend: 'indexeddb',
|
||||
});
|
||||
render(<BackendIndicator />);
|
||||
// Tooltip content is rendered in TooltipProvider
|
||||
expect(screen.getByTestId('backend-indicator')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders tooltip for Flask backend', () => {
|
||||
test('renders status for Flask backend', () => {
|
||||
(storageModule.getStorageConfig as jest.Mock).mockReturnValue({
|
||||
backend: 'flask',
|
||||
});
|
||||
@@ -152,16 +163,16 @@ describe('BackendIndicator', () => {
|
||||
expect(indicator).toHaveTextContent('Local');
|
||||
});
|
||||
|
||||
test('renders with proper font weight', () => {
|
||||
test('renders status badge component', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('backend-indicator');
|
||||
const indicator = screen.getByTestId('status-badge');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders flex container for alignment', () => {
|
||||
test('renders wrapper element', () => {
|
||||
render(<BackendIndicator />);
|
||||
const indicator = screen.getByTestId('backend-indicator');
|
||||
expect(indicator).toHaveClass('flex');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,56 +1,44 @@
|
||||
import { Database, CloudCheck } from '@phosphor-icons/react'
|
||||
import { getStorageConfig } from '@/lib/storage'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
'use client'
|
||||
|
||||
import { BackendStatus, statusIndicatorStyles } from '@metabuilder/components'
|
||||
import { getStorageConfig } from '@/lib/storage'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
// Inject status indicator styles (only once)
|
||||
if (typeof document !== 'undefined') {
|
||||
const styleId = 'backend-status-styles'
|
||||
if (!document.getElementById(styleId)) {
|
||||
const styleTag = document.createElement('style')
|
||||
styleTag.id = styleId
|
||||
styleTag.textContent = statusIndicatorStyles
|
||||
document.head.appendChild(styleTag)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend connection indicator component.
|
||||
* Uses the shared BackendStatus component from @metabuilder/components.
|
||||
* Reads storage configuration and displays connection status.
|
||||
*/
|
||||
export function BackendIndicator() {
|
||||
const { backend } = getStorageConfig()
|
||||
const isEnvConfigured = Boolean(process.env.NEXT_PUBLIC_FLASK_BACKEND_URL)
|
||||
|
||||
if (backend === 'indexeddb') {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
data-testid="backend-indicator"
|
||||
className="backend-indicator disconnected flex items-center gap-2 px-3 py-1.5 rounded-full bg-muted/50 border border-border"
|
||||
role="status"
|
||||
aria-label="Backend connection status: Local storage"
|
||||
>
|
||||
<Database size={16} className="text-muted-foreground" aria-hidden="true" />
|
||||
<span className="text-xs font-medium text-muted-foreground">Local</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Disconnected from backend - Using local storage</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
// Determine status based on backend type
|
||||
const status = backend === 'indexeddb' ? 'disconnected' : 'connected'
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
data-testid="backend-indicator"
|
||||
className="backend-indicator connected flex items-center gap-2 px-3 py-1.5 rounded-full bg-accent/10 border border-accent/30"
|
||||
role="status"
|
||||
aria-label={`Backend connection status: Connected${isEnvConfigured ? ' (Auto-configured)' : ''}`}
|
||||
>
|
||||
<CloudCheck size={16} className="text-accent" weight="fill" aria-hidden="true" />
|
||||
<span className="text-xs font-medium text-accent">Connected</span>
|
||||
{isEnvConfigured && (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Connected to Flask Backend</p>
|
||||
{isEnvConfigured && <p className="text-xs text-muted-foreground">Auto-configured</p>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div data-testid="backend-indicator">
|
||||
<BackendStatus
|
||||
status={status}
|
||||
showDot={status === 'connected' && isEnvConfigured}
|
||||
disconnectedTooltip="Disconnected from backend - Using local storage"
|
||||
connectedTooltip={
|
||||
isEnvConfigured
|
||||
? 'Connected to Flask Backend (Auto-configured)'
|
||||
: 'Connected to Flask Backend'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"dependencies": {
|
||||
"@metabuilder/api-clients": "file:../../redux/api-clients",
|
||||
"@metabuilder/core-hooks": "file:../../redux/core-hooks",
|
||||
"@metabuilder/components": "file:../components",
|
||||
"@metabuilder/fakemui": "file:../fakemui",
|
||||
"@reduxjs/toolkit": "^2.5.2",
|
||||
"axios": "^1.7.7",
|
||||
|
||||
@@ -9,7 +9,7 @@ import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from '../../store/store';
|
||||
import MainLayout from './MainLayout';
|
||||
import { NotificationContainer } from '../UI/NotificationContainer';
|
||||
import { NotificationAdapter } from '../UI/NotificationAdapter';
|
||||
import { LoadingOverlay } from '../UI/LoadingOverlay';
|
||||
import { AuthInitializer } from '../Auth/AuthInitializer';
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function RootLayoutClient({ children }: RootLayoutClientProps) {
|
||||
<MainLayout showSidebar={true}>
|
||||
{children}
|
||||
</MainLayout>
|
||||
<NotificationContainer />
|
||||
<NotificationAdapter />
|
||||
<LoadingOverlay />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
62
workflowui/src/components/UI/NotificationAdapter.tsx
Normal file
62
workflowui/src/components/UI/NotificationAdapter.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Notification Adapter Component
|
||||
* Bridges the useUI() Redux hook to the shared NotificationContainer component
|
||||
*
|
||||
* This adapter connects workflowui's Redux-based notification state
|
||||
* to the framework-agnostic NotificationContainer from @metabuilder/components
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
NotificationContainer,
|
||||
type NotificationData,
|
||||
type NotificationPosition,
|
||||
} from '@metabuilder/components';
|
||||
import { useUINotifications } from '../../hooks/ui';
|
||||
|
||||
export interface NotificationAdapterProps {
|
||||
/** Position on screen - defaults to top-right */
|
||||
position?: NotificationPosition;
|
||||
/** Maximum notifications to show at once */
|
||||
maxVisible?: number;
|
||||
/** Custom className for container */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification adapter that connects Redux state to the shared component
|
||||
*
|
||||
* @example
|
||||
* // In your layout:
|
||||
* <NotificationAdapter position="top-right" maxVisible={5} />
|
||||
*/
|
||||
export const NotificationAdapter: React.FC<NotificationAdapterProps> = ({
|
||||
position = 'top-right',
|
||||
maxVisible = 5,
|
||||
className,
|
||||
}) => {
|
||||
const { notifications, removeNotification } = useUINotifications();
|
||||
|
||||
// Map workflowui Notification type to shared NotificationData type
|
||||
// Both types are compatible - id, type, message, duration
|
||||
const mappedNotifications: NotificationData[] = notifications.map((n) => ({
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
message: n.message,
|
||||
duration: n.duration,
|
||||
}));
|
||||
|
||||
return (
|
||||
<NotificationContainer
|
||||
notifications={mappedNotifications}
|
||||
onClose={removeNotification}
|
||||
position={position}
|
||||
maxVisible={maxVisible}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationAdapter;
|
||||
@@ -1,6 +1,14 @@
|
||||
/**
|
||||
* Notification Container Component
|
||||
* Displays notifications from Redux state
|
||||
* @deprecated This component is deprecated in favor of the shared NotificationContainer
|
||||
* from @metabuilder/components. Use NotificationAdapter instead, which bridges
|
||||
* the Redux state to the shared component.
|
||||
*
|
||||
* Migration guide:
|
||||
* - Replace imports of NotificationContainer with NotificationAdapter
|
||||
* - The adapter automatically connects to useUI() hook
|
||||
*
|
||||
* @see /src/components/UI/NotificationAdapter.tsx
|
||||
* @see @metabuilder/components NotificationContainer
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
Reference in New Issue
Block a user