- 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>
39 KiB
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 classjs/components/debug-panel.js- DebugPanel classjs/components/dom-renderer.js- DOMRenderer class + FloatingMenujs/core/section-manager.js- SectionManager class
- Comprehensive TDD test suite exists
- Modular directory structure established
❌ Current Issues
- No ES6 modules: No import/export system, using global classes
- Tight coupling: Components directly instantiate each other (
new DOMRenderer()) - No inheritance hierarchy: All classes are standalone
- Embedded dependencies: FloatingMenu is inside DOMRenderer
- No plugin system: Static loading, no lazy initialization
- Manual dependency management: No automatic loading/injection
- 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
- Add new plugin infrastructure alongside existing code
- Set up legacy bridge for backward compatibility
- Update build process to include new modules
- Create migration testing framework
Phase 5b: Widget-by-Widget Migration (Days 2-3)
Migration order (least dependent first):
- DebugPanel → Migrate first (no dependencies)
- DocumentControls → Migrate second (minimal dependencies)
- FloatingMenu → Extract from DOMRenderer
- TextEditor/ImageEditor → Migrate edit functionality
- DOMRenderer → Migrate remaining functionality
- SectionManager → Migrate core logic
For each widget:
- Create plugin definition
- Migrate widget to new base classes
- Update tests to use new system
- Add legacy compatibility shim
- Verify existing functionality works
Phase 5c: Testing and Validation (Throughout)
- Run existing test suite with legacy bridge
- Add new plugin-specific tests
- Test hot reloading functionality
- Performance testing with lazy loading
Phase 5d: Cleanup (After migration)
- Remove legacy bridge once all code migrated
- Remove old monolithic files
- Update documentation
- Performance optimization
Phase 6: Advanced Features (2 days)
6.1 Hot Reloading for Development
File: js/core/hot-reload.js
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
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:
// 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:
// 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.