# 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 = `
Plugin Dev Tools
`; 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.