A flexible panel system for building extensible web applications with support for internal and external panel extensions
npm install @principal-ade/panel-framework-coreA flexible, extensible panel system for building modular web applications with React. Supports both internal panels (living in your codebase) and external panels (distributed via NPM).
- šÆ Type-safe - Full TypeScript support with comprehensive type definitions
- š Pluggable - Support for both internal and external panel extensions
- šØ Flexible - Minimal opinions, maximum flexibility
- š Modern - Built with React hooks and modern ES modules
- š¦ Zero dependencies - Only peer dependencies on React
- šŖ Event-driven - Built-in event bus for inter-panel communication
- š”ļø Error boundaries - Automatic error handling and recovery
- ā” Lazy loading - Built-in support for code splitting
``bash`
npm install @principal-ade/panel-framework-coreor
bun add @principal-ade/panel-framework-core
`bash`
npm install react react-dom
`typescript
// src/panels/MyPanel.tsx
import type { PanelComponentProps } from '@principal-ade/panel-framework-core';
export const MyPanel: React.FC Repository: {context.repositoryPath}
return (
My Panel
);
};
// Panel metadata
export const metadata = {
id: 'my-panel',
name: 'My Panel',
description: 'A simple example panel',
};
export default MyPanel;
`
`typescript
// src/App.tsx
import { PanelHarness, PanelWrapper, PanelEventBus, globalPanelRegistry } from '@principal-ade/panel-framework-core';
import { MyPanel, metadata } from './panels/MyPanel';
// Register panel
globalPanelRegistry.register({
metadata,
component: MyPanel,
});
function App() {
const context = {
repositoryPath: '/my/repo',
repository: null,
data: {},
loading: false,
refresh: async () => {},
hasSlice: () => false,
isSliceLoading: () => false,
};
const actions = {
openFile: (path: string) => console.log('Open file:', path),
};
const events = new PanelEventBus();
return (
);
}
`
`typescript
// src/App.tsx
import { globalPanelRegistry, PanelWrapper } from '@principal-ade/panel-framework-core';
function PanelContainer({ panelId }: { panelId: string }) {
const [panel, setPanel] = React.useState(null);
React.useEffect(() => {
globalPanelRegistry.get(panelId).then(setPanel);
}, [panelId]);
if (!panel) return
return (
lifecycle={panel.lifecycle}
/>
);
}
`
> Source: src/components/PanelHarness.tsx, src/components/PanelWrapper.tsx
#### PanelHarness
The main context provider for the panel system. See src/components/PanelHarness.tsx.
`typescript`
actions={actionsObject}
events={eventBusInstance}
>
{children}
Props:
- context: PanelContextValue - The context object available to all panelsactions?: PanelActions
- - Optional actions for inter-panel communicationevents?: PanelEventEmitter
- - Optional event bus for panel eventschildren: ReactNode
- - Child components (usually PanelWrapper components)
#### PanelWrapper
Wraps individual panels with lifecycle management and error boundaries. See src/components/PanelWrapper.tsx.
`typescript`
lifecycle={lifecycleHooks}
fallback={
errorFallback={ErrorComponent}
/>
Props:
- component: ComponentType - The panel componentlifecycle?: PanelLifecycleHooks
- - Optional lifecycle hooksfallback?: ReactNode
- - Loading fallbackerrorFallback?: ComponentType<{ error: Error; reset: () => void }>
- - Error fallback
> Source: src/components/PanelHarness.tsx (hooks are exported from the harness)
#### usePanelContext()
Access the panel context.
`typescript`
const context = usePanelContext();
console.log(context.repositoryPath);
#### usePanelActions()
Access panel actions.
`typescript`
const actions = usePanelActions();
actions.openFile?.('README.md');
#### usePanelEvents()
Access the event emitter.
`typescript
const events = usePanelEvents();
useEffect(() => {
const unsubscribe = events.on('file:opened', (event) => {
console.log('File opened:', event.payload);
});
return unsubscribe;
}, [events]);
`
#### PanelEventBus
> Source: src/events/PanelEventBus.ts
Browser-compatible event bus for inter-panel communication.
`typescript
const eventBus = new PanelEventBus();
// Emit an event
eventBus.emit({
type: 'file:opened',
source: 'my-panel',
timestamp: Date.now(),
payload: { path: '/README.md' },
});
// Listen to events
const unsubscribe = eventBus.on('file:opened', (event) => {
console.log('File opened:', event.payload);
});
// Cleanup
unsubscribe();
`
#### PanelRegistry
> Source: src/utils/panelRegistry.ts
Manages panel registration and loading.
`typescript
import { globalPanelRegistry } from '@principal-ade/panel-framework-core';
// Register a panel
globalPanelRegistry.register({
metadata: { id: 'my-panel', name: 'My Panel' },
component: MyPanel,
onMount: async (context) => {
console.log('Panel mounted');
},
});
// Register with lazy loading
globalPanelRegistry.registerLazy('lazy-panel', async () => {
const module = await import('./panels/LazyPanel');
return module;
});
// Get a panel
const panel = await globalPanelRegistry.get('my-panel');
// Get all panel IDs
const ids = globalPanelRegistry.getAllIds();
`
> Source: src/types/panel.types.ts
Props passed to every panel component.
`typescript`
interface PanelComponentProps {
context: PanelContextValue;
actions: PanelActions;
events: PanelEventEmitter;
}
The context object available to all panels.
`typescript`
interface PanelContextValue {
repositoryPath: string | null;
repository: unknown | null;
data: Record
loading: boolean;
refresh: () => Promise
hasSlice: (slice: PanelDataSlice) => boolean;
isSliceLoading: (slice: PanelDataSlice) => boolean;
}
Panel metadata structure.
`typescript`
interface PanelMetadata {
id: string;
name: string;
icon?: string;
version?: string;
author?: string;
description?: string;
surfaces?: string[];
slices?: PanelDataSlice[];
}
Optional lifecycle hooks for panels.
`typescript`
interface PanelLifecycleHooks {
onMount?: (context: PanelContextValue) => void | Promise
onUnmount?: (context: PanelContextValue) => void | Promise
onDataChange?: (slice: PanelDataSlice, data: unknown) => void;
}
For panels living in your codebase:
`typescript
// src/panels/MyPanel/index.tsx
import type { PanelComponentProps, PanelMetadata } from '@principal-ade/panel-framework-core';
export const metadata: PanelMetadata = {
id: 'my-app.my-panel',
name: 'My Panel',
icon: 'šØ',
version: '1.0.0',
};
export const MyPanel: React.FC
// Your panel implementation
return My Panel;
};
export default MyPanel;
`
For panels distributed via NPM, follow the complete specification in PANEL_EXTENSION_STORE_SPECIFICATION.md.
Built-in event types for inter-panel communication:
- file:opened - A file was openedfile:saved
- - A file was savedfile:deleted
- - A file was deletedgit:status-changed
- - Git status changedgit:commit
- - A commit was madegit:branch-changed
- - Git branch changedpanel:focus
- - A panel received focuspanel:blur
- - A panel lost focusdata:refresh
- - Data refresh requested
See the examples directory for complete working examples:
- Basic panel setup
- Multiple panels with communication
- Lazy-loaded panels
- Custom data slices
- Error handling
```
index.ts # Main entry point, re-exports all public APIs
src/
āāā components/
ā āāā index.ts # Component exports
ā āāā PanelHarness.tsx # Context provider and hooks
ā āāā PanelWrapper.tsx # Panel wrapper with error boundaries
āāā events/
ā āāā index.ts # Event exports
ā āāā PanelEventBus.ts # Browser-compatible event bus
āāā types/
ā āāā index.ts # Type exports
ā āāā panel.types.ts # All TypeScript interfaces and types
āāā utils/
ā āāā index.ts # Utility exports
ā āāā panelRegistry.ts # Panel registration and discovery
ā āāā dataSlice.ts # Data slice utilities
āāā tools/
āāā index.ts # Tools exports
āāā PanelToolRegistry.ts # Agent tool registry
This package is designed to support a phased approach to panel extensibility:
Phase 1 (Current):
- Panels live in the codebase
- Shared panel system and types
- Event-based communication
Phase 2 (Future):
- External NPM panel packages
- Dynamic panel discovery
- Panel marketplace/store
For detailed architecture information, see:
- PANEL_EXTENSION_STORE_SPECIFICATION.md
- WEB_PANEL_SYSTEM_IMPLEMENTATION_ROADMAP.md
MIT
Contributions welcome! Please read our contributing guidelines first.