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
- 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>
1275 lines
39 KiB
Markdown
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. |