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

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

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

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 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

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)

  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

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.