Files
markitect-main/docs/WIDGET_PLUGIN_INFRASTRUCTURE_WORKPLAN.md
tegwick b963940144
Some checks failed
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
docs: add DocumentNavigator development infrastructure and test suite
- Add comprehensive widget plugin infrastructure documentation and workplan
- Include complete DocumentNavigator integration documentation
- Add TDD test suite with 15 comprehensive test cases for DocumentNavigator
- Include widget base classes (Widget, UIWidget) for future development
- Add DocumentNavigator plugin definition following planned architecture
- Include test runner and demo pages for development validation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 19:41:18 +01:00

1275 lines
39 KiB
Markdown

# Widget Plugin Infrastructure Workplan
**Document Version**: 1.0
**Date**: 2025-11-10
**Author**: Claude Code
**Objective**: Transform Markitect's UI widgets into a plugin-based architecture with lazy loading and class hierarchy
## Executive Summary
This workplan outlines the transformation of Markitect's current partially-modular JavaScript widget system into a comprehensive plugin infrastructure. The goal is to achieve:
- **Plugin-based architecture** with lazy loading capabilities
- **Class hierarchy with mixin support** to minimize code duplication
- **Isolation between widgets** to prevent breaking changes
- **Performance optimization** through code splitting and dynamic loading
- **Developer productivity** enhancements with hot reloading
## UI Positioning Convention
**Established Convention** for UI controls positioning:
### View Mode Controls
- **Position**: Left border (`left: 20px`)
- **Purpose**: Navigation and document interaction when viewing
- **Examples**: DocumentNavigator, scroll indicators
- **Appearance**: Closed floating elements initially
### Edit Mode Controls
- **Position**: Right border (`right: 20px`)
- **Purpose**: Editor-specific functionality and document manipulation
- **Examples**: Document controls (save, reset), debug panel
- **Appearance**: Closed floating elements initially
### Content-Anchored Controls
- **Position**: Relative to their content location
- **Purpose**: Direct editing of specific content sections
- **Examples**: Section editors, image editors, inline dialogs
- **Appearance**: Positioned contextually near the content they affect
This convention ensures clear separation between viewing and editing interfaces while maintaining consistency across all modes.
## Current State Analysis
### ✅ **What's Working**
- Widgets are partially separated into individual files:
- `js/components/document-controls.js` - DocumentControls class
- `js/components/debug-panel.js` - DebugPanel class
- `js/components/dom-renderer.js` - DOMRenderer class + FloatingMenu
- `js/core/section-manager.js` - SectionManager class
- Comprehensive TDD test suite exists
- Modular directory structure established
### ❌ **Current Issues**
1. **No ES6 modules**: No import/export system, using global classes
2. **Tight coupling**: Components directly instantiate each other (`new DOMRenderer()`)
3. **No inheritance hierarchy**: All classes are standalone
4. **Embedded dependencies**: FloatingMenu is inside DOMRenderer
5. **No plugin system**: Static loading, no lazy initialization
6. **Manual dependency management**: No automatic loading/injection
7. **Legacy monolith**: 5000+ lines still in `static/editor.js`
## Architecture Overview
### **Target Architecture**
```
markitect/static/js/
├── core/ # Core plugin system
│ ├── plugin-system.js # Main plugin registry and loader
│ ├── module-loader.js # Dynamic module loading
│ ├── dependency-resolver.js # Dependency graph management
│ └── hot-reload.js # Development hot reloading
├── widgets/ # Widget class hierarchy
│ ├── base/ # Base widget classes
│ │ ├── Widget.js # Root widget class
│ │ ├── UIWidget.js # UI-enabled widget
│ │ └── InteractiveWidget.js # Interactive widget
│ ├── panels/ # Panel-type widgets
│ │ ├── FloatingPanel.js # Base floating panel
│ │ ├── ControlPanel.js # Document controls
│ │ └── DebugPanel.js # Debug display
│ ├── editors/ # Editor-type widgets
│ │ ├── Editor.js # Base editor
│ │ ├── TextEditor.js # Markdown text editor
│ │ └── ImageEditor.js # Image editor with drag/drop
│ └── menus/ # Menu-type widgets
│ ├── Menu.js # Base menu
│ ├── FloatingMenu.js # Floating context menu
│ └── ContextMenu.js # Right-click menu
├── mixins/ # Reusable functionality
│ ├── Draggable.js # Drag & drop behavior
│ ├── Resizable.js # Resize handles
│ ├── Themeable.js # Theme application
│ └── Focusable.js # Focus management
├── plugins/ # Plugin definitions
│ ├── document-controls-plugin.js
│ ├── debug-panel-plugin.js
│ ├── text-editor-plugin.js
│ └── image-editor-plugin.js
├── config/ # Configuration
│ ├── widget-registry.js # Widget registration
│ └── plugin-config.js # Default configurations
└── compat/ # Legacy compatibility
├── legacy-bridge.js # Backward compatibility
└── migration-helpers.js # Migration utilities
```
## Implementation Plan
### **Phase 1: Foundation Infrastructure** (2-3 days)
#### **1.1 Core Plugin System**
**File**: `js/core/plugin-system.js`
```javascript
class PluginRegistry {
constructor() {
this.plugins = new Map();
this.dependencies = new Map();
this.loaded = new Set();
this.instances = new Map();
}
async register(name, pluginDefinition) {
// Register plugin with metadata and dependencies
this.plugins.set(name, pluginDefinition);
this.dependencies.set(name, pluginDefinition.dependencies || []);
}
async load(name, options = {}) {
// Lazy load with dependency resolution
if (this.loaded.has(name)) {
return this.createInstance(name, options);
}
await this.resolveDependencies(name);
const plugin = await this.loadPlugin(name);
this.loaded.add(name);
return this.createInstance(name, options);
}
async unload(name) {
// Clean unloading with dependency checking
}
}
```
#### **1.2 Base Widget Classes**
**File**: `js/widgets/base/Widget.js`
```javascript
export class Widget extends EventTarget {
constructor(options = {}) {
super();
this.id = options.id || `widget-${Date.now()}`;
this.container = options.container;
this.state = new Map();
this.mixins = [];
this.config = { ...this.getDefaultConfig(), ...options };
}
// Lifecycle methods
async initialize() { /* Abstract */ }
async destroy() { /* Cleanup resources */ }
// State management
setState(key, value) { /* State updates with events */ }
getState(key) { /* State retrieval */ }
// Event management
emit(event, data) { /* Custom event emission */ }
// Mixin support
applyMixin(mixin) { /* Apply mixin functionality */ }
getDefaultConfig() { return {}; }
}
```
**File**: `js/widgets/base/UIWidget.js`
```javascript
export class UIWidget extends Widget {
constructor(options) {
super(options);
this.element = null;
this.isVisible = false;
this.theme = options.theme || 'default';
this.cssClasses = new Set();
}
// UI lifecycle
async render() { /* Abstract - must implement */ }
async show() { /* Show widget */ }
async hide() { /* Hide widget */ }
async destroy() { /* Cleanup DOM */ }
// CSS and theming
addClass(className) { /* Add CSS class */ }
removeClass(className) { /* Remove CSS class */ }
applyTheme(theme) { /* Apply theme styling */ }
// DOM helpers
createElement(tag, options = {}) { /* Create styled element */ }
findElement(selector) { /* Find child element */ }
}
```
**File**: `js/widgets/base/InteractiveWidget.js`
```javascript
export class InteractiveWidget extends UIWidget {
constructor(options) {
super(options);
this.handlers = new Map();
this.shortcuts = new Map();
this.isInteractionEnabled = true;
}
// Event handling
bindEvent(element, event, handler, options = {}) { /* Bind with cleanup */ }
unbindEvent(element, event, handler) { /* Clean unbinding */ }
bindShortcut(keys, handler) { /* Keyboard shortcuts */ }
// Interaction states
enable() { /* Enable interactions */ }
disable() { /* Disable interactions */ }
// Focus management
focus() { /* Focus widget */ }
blur() { /* Blur widget */ }
}
```
#### **1.3 Mixin System**
**File**: `js/mixins/Draggable.js`
```javascript
export const Draggable = {
makeDraggable(options = {}) {
this.dragConfig = {
handle: options.handle || this.element,
constraint: options.constraint,
onStart: options.onStart,
onDrag: options.onDrag,
onEnd: options.onEnd
};
this.bindEvent(this.dragConfig.handle, 'mousedown', this.onDragStart);
this.isDraggable = true;
},
onDragStart(e) {
if (!this.isDraggable) return;
this.dragState = {
startX: e.clientX,
startY: e.clientY,
initialX: parseInt(this.element.style.left) || 0,
initialY: parseInt(this.element.style.top) || 0
};
document.addEventListener('mousemove', this.onDrag);
document.addEventListener('mouseup', this.onDragEnd);
if (this.dragConfig.onStart) {
this.dragConfig.onStart.call(this, e);
}
},
onDrag(e) { /* Drag implementation */ },
onDragEnd(e) { /* End drag implementation */ }
};
```
**File**: `js/mixins/Resizable.js`
```javascript
export const Resizable = {
makeResizable(options = {}) {
this.resizeConfig = {
handles: options.handles || ['se'], // southeast by default
minWidth: options.minWidth || 100,
minHeight: options.minHeight || 100,
onResize: options.onResize
};
this.createResizeHandles();
this.isResizable = true;
},
createResizeHandles() { /* Create resize handle elements */ },
onResizeStart(e) { /* Start resize */ },
onResize(e) { /* During resize */ },
onResizeEnd(e) { /* End resize */ }
};
```
**File**: `js/mixins/Themeable.js`
```javascript
export const Themeable = {
applyTheme(themeName) {
this.currentTheme = themeName;
const themeCSS = this.getThemeCSS(themeName);
this.updateStylesheet(themeCSS);
this.emit('theme-changed', { theme: themeName });
},
getThemeCSS(theme) {
// Return theme-specific CSS
const themes = {
default: { /* default styles */ },
dark: { /* dark theme styles */ },
light: { /* light theme styles */ }
};
return themes[theme] || themes.default;
},
updateStylesheet(styles) { /* Apply styles to widget */ }
};
```
### **Phase 2: Widget Hierarchy Design** (1-2 days)
#### **2.1 Panel Widgets**
**File**: `js/widgets/panels/FloatingPanel.js`
```javascript
import { InteractiveWidget } from '../base/InteractiveWidget.js';
import { Draggable } from '../../mixins/Draggable.js';
import { Themeable } from '../../mixins/Themeable.js';
export class FloatingPanel extends InteractiveWidget {
constructor(options) {
super(options);
this.position = options.position || { top: 20, right: 20 };
this.isFloating = true;
// Apply mixins
this.applyMixin(Draggable);
this.applyMixin(Themeable);
}
async render() {
this.element = this.createElement('div', {
className: 'floating-panel',
style: {
position: 'fixed',
top: `${this.position.top}px`,
right: `${this.position.right}px`,
zIndex: 1000,
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
backdropFilter: 'blur(8px)'
}
});
this.createHeader();
this.createBody();
this.createFooter();
// Make draggable
this.makeDraggable({ handle: this.header });
document.body.appendChild(this.element);
return this.element;
}
createHeader() { /* Header with title and controls */ }
createBody() { /* Main content area */ }
createFooter() { /* Optional footer */ }
}
```
**File**: `js/widgets/panels/ControlPanel.js`
```javascript
import { FloatingPanel } from './FloatingPanel.js';
export class ControlPanel extends FloatingPanel {
constructor(options) {
super(options);
this.buttons = new Map();
}
getDefaultConfig() {
return {
title: 'Document Controls',
position: { top: 20, right: 20 },
buttons: [
{ id: 'save', text: '💾 Save Document', color: '#28a745' },
{ id: 'reset', text: '🔄 Reset All', color: '#ffc107' },
{ id: 'status', text: '📊 Show Status', color: '#17a2b8' },
{ id: 'debug', text: '🔍 Debug', color: '#6c757d' }
]
};
}
createBody() {
this.body = this.createElement('div', {
className: 'control-panel-body'
});
this.config.buttons.forEach(buttonConfig => {
this.addButton(buttonConfig);
});
this.element.appendChild(this.body);
}
addButton(config) {
const button = this.createElement('button', {
textContent: config.text,
className: 'control-button',
style: { backgroundColor: config.color }
});
this.bindEvent(button, 'click', () => {
this.emit('button-click', { id: config.id });
});
this.buttons.set(config.id, button);
this.body.appendChild(button);
return button;
}
}
```
#### **2.2 Editor Widgets**
**File**: `js/widgets/editors/Editor.js`
```javascript
import { InteractiveWidget } from '../base/InteractiveWidget.js';
export class Editor extends InteractiveWidget {
constructor(options) {
super(options);
this.content = options.content || '';
this.originalContent = this.content;
this.isEditing = false;
this.isDirty = false;
}
// Content management
setContent(content) {
this.content = content;
this.isDirty = (content !== this.originalContent);
this.emit('content-changed', { content, isDirty: this.isDirty });
}
getContent() {
return this.content;
}
// Edit lifecycle
async startEditing() {
this.isEditing = true;
this.emit('edit-start');
}
async stopEditing(save = false) {
if (save) {
this.originalContent = this.content;
this.isDirty = false;
} else {
this.content = this.originalContent;
}
this.isEditing = false;
this.emit('edit-end', { saved: save });
}
// Abstract methods
async render() { /* Must implement */ }
focus() { /* Must implement */ }
}
```
**File**: `js/widgets/editors/TextEditor.js`
```javascript
import { Editor } from './Editor.js';
export class TextEditor extends Editor {
constructor(options) {
super(options);
this.autoResize = options.autoResize !== false;
this.placeholder = options.placeholder || 'Enter text...';
}
async render() {
this.element = this.createElement('div', {
className: 'text-editor'
});
this.textarea = this.createElement('textarea', {
value: this.content,
placeholder: this.placeholder,
className: 'text-editor-input'
});
this.bindEvent(this.textarea, 'input', this.onInput);
this.bindEvent(this.textarea, 'keydown', this.onKeyDown);
if (this.autoResize) {
this.setupAutoResize();
}
this.createToolbar();
this.element.appendChild(this.textarea);
return this.element;
}
onInput(e) {
this.setContent(e.target.value);
if (this.autoResize) {
this.resizeToContent();
}
}
onKeyDown(e) {
// Handle shortcuts (Ctrl+Enter, Escape, etc.)
if (e.ctrlKey && e.key === 'Enter') {
this.stopEditing(true);
} else if (e.key === 'Escape') {
this.stopEditing(false);
}
}
setupAutoResize() { /* Auto-resize textarea logic */ }
createToolbar() { /* Editor toolbar */ }
focus() { this.textarea.focus(); }
}
```
### **Phase 3: Lazy Loading System** (2 days)
#### **3.1 Module Loader**
**File**: `js/core/module-loader.js`
```javascript
export class ModuleLoader {
constructor() {
this.cache = new Map();
this.loading = new Map();
this.registry = new Map();
}
register(name, importFn) {
this.registry.set(name, importFn);
}
async loadWidget(widgetName, options = {}) {
// Check cache first
if (this.cache.has(widgetName)) {
return this.createInstance(widgetName, options);
}
// Check if already loading
if (this.loading.has(widgetName)) {
await this.loading.get(widgetName);
return this.createInstance(widgetName, options);
}
// Start loading
const loadPromise = this.loadModule(widgetName);
this.loading.set(widgetName, loadPromise);
try {
const module = await loadPromise;
this.cache.set(widgetName, module);
this.loading.delete(widgetName);
return this.createInstance(widgetName, options);
} catch (error) {
this.loading.delete(widgetName);
throw new Error(`Failed to load widget '${widgetName}': ${error.message}`);
}
}
async loadModule(widgetName) {
const importFn = this.registry.get(widgetName);
if (!importFn) {
throw new Error(`Widget '${widgetName}' not registered`);
}
const module = await importFn();
return module.default || module;
}
createInstance(widgetName, options) {
const WidgetClass = this.cache.get(widgetName);
return new WidgetClass(options);
}
unload(widgetName) {
this.cache.delete(widgetName);
this.loading.delete(widgetName);
}
}
```
#### **3.2 Dependency Resolution**
**File**: `js/core/dependency-resolver.js`
```javascript
export class DependencyResolver {
constructor(moduleLoader) {
this.moduleLoader = moduleLoader;
this.dependencyGraph = new Map();
this.resolved = new Set();
}
registerDependencies(widgetName, dependencies) {
this.dependencyGraph.set(widgetName, dependencies || []);
}
async resolveDependencies(widgetName, visited = new Set()) {
// Circular dependency detection
if (visited.has(widgetName)) {
throw new Error(`Circular dependency detected: ${Array.from(visited).join(' -> ')} -> ${widgetName}`);
}
// Already resolved
if (this.resolved.has(widgetName)) {
return [];
}
visited.add(widgetName);
const dependencies = this.dependencyGraph.get(widgetName) || [];
const resolved = [];
// Resolve each dependency recursively
for (const dep of dependencies) {
const depResolved = await this.resolveDependencies(dep, visited);
resolved.push(...depResolved);
if (!this.resolved.has(dep)) {
const depInstance = await this.moduleLoader.loadWidget(dep);
resolved.push(depInstance);
this.resolved.add(dep);
}
}
visited.delete(widgetName);
return resolved;
}
getDependencyOrder(widgetName) {
// Returns topologically sorted dependency order
const visited = new Set();
const order = [];
const visit = (name) => {
if (visited.has(name)) return;
visited.add(name);
const deps = this.dependencyGraph.get(name) || [];
deps.forEach(visit);
order.push(name);
};
visit(widgetName);
return order;
}
}
```
### **Phase 4: Plugin Definitions** (1-2 days)
#### **4.1 Widget Plugin Registration**
**File**: `js/plugins/document-controls-plugin.js`
```javascript
export default {
name: 'DocumentControls',
version: '1.0.0',
description: 'Document control panel with save, reset, and status functions',
author: 'Markitect Core',
// Dependencies that must be loaded first
dependencies: ['FloatingPanel', 'Button'],
// Mixins to apply
mixins: ['Draggable', 'Themeable', 'Focusable'],
// Lazy load the actual widget class
async load() {
const { ControlPanel } = await import('../widgets/panels/ControlPanel.js');
return ControlPanel;
},
// Default configuration
defaultOptions: {
position: { top: 20, right: 20 },
theme: 'default',
draggable: true,
title: 'Document Controls',
buttons: [
{ id: 'save-document', text: '💾 Save Document', color: '#28a745' },
{ id: 'reset-all', text: '🔄 Reset All', color: '#ffc107' },
{ id: 'show-status', text: '📊 Show Status', color: '#17a2b8' },
{ id: 'toggle-debug', text: '🔍 Debug', color: '#6c757d' }
]
},
// Plugin lifecycle hooks
async onLoad(instance, options) {
// Called after widget is loaded and created
console.log(`DocumentControls plugin loaded with options:`, options);
},
async onUnload(instance) {
// Called before widget is destroyed
console.log('DocumentControls plugin unloading');
},
// Feature flags and capabilities
capabilities: {
draggable: true,
resizable: false,
themeable: true,
persistent: true // Survives page reloads
}
};
```
#### **4.2 Widget Registry Configuration**
**File**: `js/config/widget-registry.js`
```javascript
export const widgetRegistry = {
// Core widgets
'DocumentControls': () => import('../plugins/document-controls-plugin.js'),
'DebugPanel': () => import('../plugins/debug-panel-plugin.js'),
'StatusModal': () => import('../plugins/status-modal-plugin.js'),
// Editor widgets
'TextEditor': () => import('../plugins/text-editor-plugin.js'),
'ImageEditor': () => import('../plugins/image-editor-plugin.js'),
'MarkdownEditor': () => import('../plugins/markdown-editor-plugin.js'),
// Menu widgets
'FloatingMenu': () => import('../plugins/floating-menu-plugin.js'),
'ContextMenu': () => import('../plugins/context-menu-plugin.js'),
'DropdownMenu': () => import('../plugins/dropdown-menu-plugin.js'),
// Utility widgets
'LoadingSpinner': () => import('../plugins/loading-spinner-plugin.js'),
'ProgressBar': () => import('../plugins/progress-bar-plugin.js'),
'Toast': () => import('../plugins/toast-plugin.js'),
};
export const pluginCategories = {
'panels': ['DocumentControls', 'DebugPanel'],
'editors': ['TextEditor', 'ImageEditor', 'MarkdownEditor'],
'menus': ['FloatingMenu', 'ContextMenu', 'DropdownMenu'],
'utilities': ['LoadingSpinner', 'ProgressBar', 'Toast']
};
export const dependencyGraph = {
'DocumentControls': ['FloatingPanel'],
'DebugPanel': ['FloatingPanel'],
'TextEditor': ['Editor'],
'ImageEditor': ['Editor'],
'FloatingMenu': ['Menu'],
'ContextMenu': ['Menu']
};
```
#### **4.3 Main Plugin System Integration**
**File**: `js/core/widget-system.js`
```javascript
import { PluginRegistry } from './plugin-system.js';
import { ModuleLoader } from './module-loader.js';
import { DependencyResolver } from './dependency-resolver.js';
import { widgetRegistry, dependencyGraph } from '../config/widget-registry.js';
export class WidgetSystem {
constructor() {
this.moduleLoader = new ModuleLoader();
this.dependencyResolver = new DependencyResolver(this.moduleLoader);
this.pluginRegistry = new PluginRegistry();
this.activeWidgets = new Map();
this.initialize();
}
async initialize() {
// Register all widgets from registry
for (const [name, importFn] of Object.entries(widgetRegistry)) {
this.moduleLoader.register(name, importFn);
this.dependencyResolver.registerDependencies(name, dependencyGraph[name]);
}
}
async createWidget(widgetName, options = {}) {
try {
// Resolve dependencies first
await this.dependencyResolver.resolveDependencies(widgetName);
// Load the widget
const widget = await this.moduleLoader.loadWidget(widgetName, options);
// Track active widgets
const id = widget.id;
this.activeWidgets.set(id, widget);
// Setup cleanup
widget.addEventListener('destroy', () => {
this.activeWidgets.delete(id);
});
return widget;
} catch (error) {
console.error(`Failed to create widget '${widgetName}':`, error);
throw error;
}
}
async destroyWidget(widgetId) {
const widget = this.activeWidgets.get(widgetId);
if (widget) {
await widget.destroy();
this.activeWidgets.delete(widgetId);
}
}
getActiveWidgets() {
return Array.from(this.activeWidgets.values());
}
getWidget(widgetId) {
return this.activeWidgets.get(widgetId);
}
}
// Global widget system instance
export const widgetSystem = new WidgetSystem();
```
### **Phase 5: Migration Strategy** (2-3 days)
#### **5.1 Backward Compatibility Layer**
**File**: `js/compat/legacy-bridge.js`
```javascript
import { widgetSystem } from '../core/widget-system.js';
/**
* Legacy compatibility bridge for existing code
* Provides old-style constructor access during migration
*/
export class LegacyBridge {
constructor() {
this.legacyInstances = new Map();
this.setupGlobalConstructors();
}
setupGlobalConstructors() {
// Provide legacy global constructors
window.DocumentControls = this.createLegacyConstructor('DocumentControls');
window.DebugPanel = this.createLegacyConstructor('DebugPanel');
window.DOMRenderer = this.createLegacyConstructor('DOMRenderer');
window.SectionManager = this.createLegacyConstructor('SectionManager');
}
createLegacyConstructor(widgetName) {
return async function(options = {}) {
console.warn(`Using legacy constructor for ${widgetName}. Consider migrating to widget system.`);
const widget = await widgetSystem.createWidget(widgetName, options);
// Store legacy reference
this.legacyInstances.set(widget.id, widget);
return widget;
}.bind(this);
}
async migrate(legacyInstance) {
// Helper to migrate existing instances
const widgetName = legacyInstance.constructor.name;
const options = this.extractOptions(legacyInstance);
const newWidget = await widgetSystem.createWidget(widgetName, options);
// Transfer state
this.transferState(legacyInstance, newWidget);
return newWidget;
}
extractOptions(legacyInstance) {
// Extract configuration from legacy instance
return {
container: legacyInstance.container,
theme: legacyInstance.theme,
// ... other extractable options
};
}
transferState(from, to) {
// Transfer state between instances
if (from.isVisible) {
to.show();
}
// ... other state transfers
}
}
```
#### **5.2 Migration Plan**
**Phase 5a: Setup (Day 1)**
1. Add new plugin infrastructure alongside existing code
2. Set up legacy bridge for backward compatibility
3. Update build process to include new modules
4. Create migration testing framework
**Phase 5b: Widget-by-Widget Migration (Days 2-3)**
Migration order (least dependent first):
1. **DebugPanel** → Migrate first (no dependencies)
2. **DocumentControls** → Migrate second (minimal dependencies)
3. **FloatingMenu** → Extract from DOMRenderer
4. **TextEditor/ImageEditor** → Migrate edit functionality
5. **DOMRenderer** → Migrate remaining functionality
6. **SectionManager** → Migrate core logic
For each widget:
1. Create plugin definition
2. Migrate widget to new base classes
3. Update tests to use new system
4. Add legacy compatibility shim
5. Verify existing functionality works
**Phase 5c: Testing and Validation (Throughout)**
1. Run existing test suite with legacy bridge
2. Add new plugin-specific tests
3. Test hot reloading functionality
4. Performance testing with lazy loading
**Phase 5d: Cleanup (After migration)**
1. Remove legacy bridge once all code migrated
2. Remove old monolithic files
3. Update documentation
4. Performance optimization
### **Phase 6: Advanced Features** (2 days)
#### **6.1 Hot Reloading for Development**
**File**: `js/core/hot-reload.js`
```javascript
export class HotReloader {
constructor(widgetSystem) {
this.widgetSystem = widgetSystem;
this.watchedModules = new Map();
this.stateSnapshots = new Map();
if (process.env.NODE_ENV === 'development') {
this.enableHotReload();
}
}
enableHotReload() {
// Listen for module update events
if (module.hot) {
module.hot.accept();
module.hot.addStatusHandler(status => {
if (status === 'idle') {
this.onModuleUpdate();
}
});
}
}
async reloadWidget(widgetName) {
console.log(`Hot reloading widget: ${widgetName}`);
// Find all active instances of this widget
const instances = this.findActiveInstances(widgetName);
// Save state snapshots
for (const instance of instances) {
this.stateSnapshots.set(instance.id, this.captureState(instance));
}
// Unload old module
this.widgetSystem.moduleLoader.unload(widgetName);
// Reload module
const newInstances = [];
for (const instance of instances) {
const snapshot = this.stateSnapshots.get(instance.id);
// Destroy old instance
await instance.destroy();
// Create new instance
const newInstance = await this.widgetSystem.createWidget(widgetName, snapshot.options);
// Restore state
this.restoreState(newInstance, snapshot.state);
newInstances.push(newInstance);
}
console.log(`Hot reload complete for ${widgetName}: ${newInstances.length} instances updated`);
return newInstances;
}
findActiveInstances(widgetName) {
return this.widgetSystem.getActiveWidgets()
.filter(widget => widget.constructor.name === widgetName);
}
captureState(widget) {
return {
options: widget.config,
state: {
isVisible: widget.isVisible,
position: widget.position,
content: widget.content,
customState: widget.state ? Object.fromEntries(widget.state) : {}
}
};
}
restoreState(widget, state) {
widget.isVisible = state.isVisible;
widget.position = state.position;
if (state.content) widget.setContent(state.content);
// Restore custom state
for (const [key, value] of Object.entries(state.customState)) {
widget.setState(key, value);
}
// Show if was visible
if (state.isVisible) {
widget.show();
}
}
}
```
#### **6.2 Plugin Development Tools**
**File**: `js/dev/plugin-dev-tools.js`
```javascript
export class PluginDevTools {
constructor(widgetSystem) {
this.widgetSystem = widgetSystem;
this.createDevPanel();
}
createDevPanel() {
if (process.env.NODE_ENV !== 'development') return;
const devPanel = document.createElement('div');
devPanel.id = 'plugin-dev-tools';
devPanel.innerHTML = `
<div class="dev-panel-header">Plugin Dev Tools</div>
<div class="dev-panel-body">
<button id="reload-all">Reload All Widgets</button>
<button id="inspect-widgets">Inspect Active Widgets</button>
<button id="performance-stats">Performance Stats</button>
<div id="widget-list"></div>
</div>
`;
this.styleDev Panel(devPanel);
document.body.appendChild(devPanel);
this.bindDevPanelEvents(devPanel);
}
async reloadAllWidgets() {
const widgets = this.widgetSystem.getActiveWidgets();
const results = [];
for (const widget of widgets) {
try {
const newWidget = await this.hotReloader.reloadWidget(widget.constructor.name);
results.push({ widget: widget.constructor.name, status: 'success' });
} catch (error) {
results.push({ widget: widget.constructor.name, status: 'error', error });
}
}
console.table(results);
return results;
}
inspectActiveWidgets() {
const widgets = this.widgetSystem.getActiveWidgets();
const inspection = widgets.map(widget => ({
id: widget.id,
type: widget.constructor.name,
visible: widget.isVisible,
state: Object.fromEntries(widget.state || [])
}));
console.table(inspection);
return inspection;
}
}
```
## Testing Strategy
### **Unit Testing**
Each widget will have comprehensive unit tests:
```javascript
// js/tests/widgets/ControlPanel.test.js
import { ControlPanel } from '../widgets/panels/ControlPanel.js';
describe('ControlPanel', () => {
let controlPanel;
beforeEach(() => {
controlPanel = new ControlPanel({
position: { top: 100, right: 100 }
});
});
afterEach(async () => {
if (controlPanel) {
await controlPanel.destroy();
}
});
test('should create with default configuration', () => {
expect(controlPanel.config.title).toBe('Document Controls');
expect(controlPanel.config.buttons).toHaveLength(4);
});
test('should render floating panel', async () => {
const element = await controlPanel.render();
expect(element).toBeInstanceOf(HTMLElement);
expect(element.classList.contains('floating-panel')).toBe(true);
});
test('should emit button-click events', async () => {
await controlPanel.render();
const eventSpy = jest.fn();
controlPanel.addEventListener('button-click', eventSpy);
const saveButton = controlPanel.buttons.get('save');
saveButton.click();
expect(eventSpy).toHaveBeenCalledWith(
expect.objectContaining({
detail: { id: 'save' }
})
);
});
});
```
### **Integration Testing**
Test widget interactions and plugin loading:
```javascript
// js/tests/integration/plugin-system.test.js
import { widgetSystem } from '../core/widget-system.js';
describe('Plugin System Integration', () => {
test('should load widget with dependencies', async () => {
const controlPanel = await widgetSystem.createWidget('DocumentControls');
expect(controlPanel).toBeDefined();
expect(controlPanel.constructor.name).toBe('ControlPanel');
});
test('should handle circular dependencies', async () => {
await expect(
widgetSystem.createWidget('CircularWidget')
).rejects.toThrow('Circular dependency detected');
});
});
```
## Performance Considerations
### **Bundle Optimization**
- **Code splitting**: Each widget is a separate chunk
- **Tree shaking**: Only load used functionality
- **Lazy loading**: Load widgets on demand
- **Caching**: Module-level caching for repeated loads
### **Memory Management**
- **Proper cleanup**: Widgets clean up event listeners and DOM
- **State management**: Efficient state storage and updates
- **Memory leaks**: Comprehensive testing for leaks
### **Loading Performance**
- **Preloading**: Predictive loading of likely-needed widgets
- **Chunking**: Optimal chunk sizing for network requests
- **Compression**: Gzipped module delivery
## Benefits Summary
### ✅ **Isolation & Stability**
- **Independent updates**: Each widget can be updated without affecting others
- **Failure isolation**: Widget crashes don't bring down the entire system
- **Version management**: Individual widget versioning and rollback capability
### ✅ **Performance**
- **Lazy loading**: 60-80% reduction in initial bundle size
- **Code splitting**: Faster page loads and better caching
- **Memory efficiency**: Unload unused widgets to free memory
### ✅ **Code Reuse**
- **Mixin system**: Share common functionality across widgets
- **Base classes**: Inherit common widget behaviors
- **Composition**: Flexible widget construction and customization
### ✅ **Developer Experience**
- **Hot reloading**: Instant feedback during development
- **Plugin discovery**: Easy exploration of available widgets
- **Testing isolation**: Test widgets independently with mocked dependencies
- **Type safety**: Full TypeScript support potential
## Risk Mitigation
### **Migration Risks**
- **Backward compatibility**: Legacy bridge ensures existing code works
- **Gradual migration**: Widget-by-widget migration minimizes risk
- **Comprehensive testing**: Existing test suite validates functionality
### **Performance Risks**
- **Bundle size**: Code splitting prevents size increases
- **Loading delays**: Preloading and caching minimize delays
- **Memory usage**: Proper cleanup prevents memory leaks
### **Complexity Risks**
- **Learning curve**: Comprehensive documentation and examples
- **Debugging**: Development tools and clear error messages
- **Maintenance**: Clean architecture reduces maintenance burden
## Estimated Timeline
| Phase | Duration | Deliverables |
|-------|----------|-------------|
| Phase 1: Foundation | 2-3 days | Core plugin system, base classes, mixins |
| Phase 2: Widget Hierarchy | 1-2 days | Panel, editor, menu widget classes |
| Phase 3: Lazy Loading | 2 days | Module loader, dependency resolver |
| Phase 4: Plugin Definitions | 1-2 days | Plugin configurations, widget registry |
| Phase 5: Migration | 2-3 days | Legacy bridge, widget-by-widget migration |
| Phase 6: Advanced Features | 2 days | Hot reloading, dev tools |
| **Total** | **8-12 days** | **Complete plugin infrastructure** |
## Success Metrics
### **Functional**
- ✅ All existing widgets work without modification
- ✅ New widgets can be added without touching existing code
- ✅ Hot reloading works for all widgets
- ✅ Plugin loading time < 100ms per widget
### **Performance**
- ✅ 60%+ reduction in initial bundle size
- ✅ <50ms widget instantiation time
- ✅ Zero memory leaks in widget lifecycle
- ✅ 100% test coverage maintained
### **Developer Experience**
- ✅ Widget development takes 50% less time
- ✅ Zero breaking changes during updates
- ✅ Complete TypeScript support
- ✅ Comprehensive documentation and examples
This plugin infrastructure will transform Markitect's UI system into a modern, maintainable, and extensible architecture that supports rapid development while ensuring stability and performance.