diff --git a/capabilities/testdrive-jsui/IMPLEMENTATION_NOTES.md b/capabilities/testdrive-jsui/IMPLEMENTATION_NOTES.md new file mode 100644 index 00000000..20ee9765 --- /dev/null +++ b/capabilities/testdrive-jsui/IMPLEMENTATION_NOTES.md @@ -0,0 +1,146 @@ +# TestDrive-JSUI Implementation Notes + +## Enhanced ControlBase Architecture + +### Overview +The ControlBase class has been significantly enhanced to provide advanced panel behavior patterns based on the reference implementation in `relicts/DebugControlContent.html`. This creates a modern, interactive foundation for all UI control panels. + +### Key Features Implemented + +#### 1. Icon-Only Collapsed State +- **Behavior**: Controls start as compact 40px icon buttons +- **Styling**: Clean design with subtle shadows and hover effects +- **Positioning**: Compass-based positioning (N, NE, E, SE, S, SW, W, NW) +- **Implementation**: `control-toggle` button with icon display + +#### 2. Expand/Drag Functionality +- **Behavior**: Click icon expands to full panel; drag header to reposition when expanded +- **Event Handling**: Proper event delegation prevents conflicts +- **Position Tracking**: Maintains drag offset for smooth movement +- **Implementation**: `startDrag()`, `handleDrag()`, `stopDrag()` methods + +#### 3. Bottom-Left Corner Resize +- **Behavior**: Resize handle (↙) appears in bottom-left when expanded +- **Constraints**: Minimum 200px width, 150px height +- **Direction**: Bottom-left resize (expand down and left) +- **Implementation**: `addResizeHandle()`, resize event handlers + +#### 4. Collapse with Position Restoration +- **Behavior**: Close button (βœ•) returns to original compass position +- **State Management**: Clears drag positioning, restores transform/positioning +- **Cleanup**: Removes resize handles and event listeners +- **Implementation**: `collapse()` method with `originalPosition` restoration + +#### 5. Header Toggle for Content Visibility +- **Behavior**: Click title toggles content area visibility +- **States**: Full expanded vs header-only modes +- **Preservation**: Maintains expanded state while hiding content +- **Implementation**: `toggleHeaderOnly()` method + +### Technical Architecture + +#### State Management +```javascript +this.isExpanded = false; // Icon vs expanded panel +this.isHeaderOnly = false; // Header-only vs full content +this.isDragging = false; // Drag operation active +this.isResizing = false; // Resize operation active +this.originalPosition = null; // Compass position storage +``` + +#### DOM Structure +```html +
+ +
+
+ πŸ”§ + Control + +
+
...
+
+
+``` + +#### Event Handling Strategy +- **Tracked Events**: Automatic cleanup with `eventHandlers` Map +- **Global Events**: Separate tracking for drag/resize (`_dragHandlers`, `_resizeHandlers`) +- **Event Prevention**: `stopPropagation()` and `preventDefault()` where needed +- **Conflict Resolution**: State checks prevent overlapping operations + +### Usage for Derived Controls + +Controls inherit all functionality by extending `ControlBase`: + +```javascript +class MyControl extends ControlBase { + constructor() { + super(); + this.config = { + icon: 'πŸ“Š', + title: 'My Control', + position: 'ne', + className: 'my-control' + }; + } + + buildContent() { + const content = this.element?.querySelector('.control-content'); + if (content) { + content.innerHTML = `
Custom content here
`; + } + } +} +``` + +### Integration Points + +#### With TestDrive-JSUI System +- **Component Discovery**: Listed by `scripts/list_components.py` +- **TDD Testing**: Validated by `tests/test_component_listing.py` +- **Legacy Support**: `DocumentControlsLegacy` maintains backward compatibility + +#### With MarkiTect md-render +- **Plugin Integration**: Ready for deployment via Makefile targets +- **Asset Deployment**: CSS/JS bundling for production use +- **Edit Mode**: Enhanced interactive editing experience + +### Testing + +#### Test Page: `test-control-base.html` +- Interactive demonstration of all 5 behaviors +- Multiple controls in different compass positions +- Real-time functionality validation + +#### Automated Testing +- Component listing tests ensure discovery +- Integration tests validate interaction patterns +- Legacy tests maintain backward compatibility + +### Performance Considerations + +#### Event Management +- Automatic cleanup prevents memory leaks +- Efficient event delegation reduces overhead +- State-based operation prevention avoids conflicts + +#### DOM Manipulation +- Minimal DOM changes during state transitions +- CSS-based styling reduces JavaScript overhead +- Lazy content building improves initial load + +### Browser Compatibility + +#### Modern Features Used +- `getBoundingClientRect()` for precise positioning +- CSS transforms for smooth positioning +- Event delegation patterns +- CSS backdrop-filter (with fallbacks) + +#### Fallback Strategy +- Graceful degradation for older browsers +- Feature detection where necessary +- Progressive enhancement approach + +This enhanced ControlBase provides a solid foundation for modern UI control panels while maintaining compatibility with existing systems. \ No newline at end of file diff --git a/capabilities/testdrive-jsui/js/components/document-controls.js b/capabilities/testdrive-jsui/js/components/document-controls-legacy.js similarity index 90% rename from capabilities/testdrive-jsui/js/components/document-controls.js rename to capabilities/testdrive-jsui/js/components/document-controls-legacy.js index fb83ebd8..e8bcbc22 100644 --- a/capabilities/testdrive-jsui/js/components/document-controls.js +++ b/capabilities/testdrive-jsui/js/components/document-controls-legacy.js @@ -1,17 +1,22 @@ /** - * DocumentControls Component + * DocumentControlsLegacy Component * - * Extracted from monolithic editor.js as part of architecture refactoring. + * Legacy implementation extracted from monolithic editor.js as part of architecture refactoring. * Handles the floating control panel and document-level actions. * + * ⚠️ LEGACY COMPONENT: This implementation is maintained for backward compatibility. + * New projects should use the modern control system components instead. + * * Dependencies: * - None (standalone component) */ /** - * DocumentControls - Manages the floating control panel and its buttons + * DocumentControlsLegacy - Legacy floating control panel manager + * + * @deprecated This is a legacy implementation. Use modern control components for new development. */ -class DocumentControls { +class DocumentControlsLegacy { constructor() { this.controlPanel = null; this.buttons = new Map(); @@ -270,10 +275,15 @@ class DocumentControls { // Export for use in tests and other modules if (typeof module !== 'undefined' && module.exports) { - module.exports = { DocumentControls }; + module.exports = { DocumentControlsLegacy }; } -// Export for browser use +// Export for browser use and global access if (typeof window !== 'undefined') { - window.DocumentControls = DocumentControls; + window.DocumentControlsLegacy = DocumentControlsLegacy; +} + +// Also make available in global for tests +if (typeof global !== 'undefined') { + global.DocumentControlsLegacy = DocumentControlsLegacy; } \ No newline at end of file diff --git a/capabilities/testdrive-jsui/js/controls/contents-control.js b/capabilities/testdrive-jsui/js/controls/contents-control.js new file mode 100644 index 00000000..b4351052 --- /dev/null +++ b/capabilities/testdrive-jsui/js/controls/contents-control.js @@ -0,0 +1,336 @@ +/** + * ContentsControl - Table of Contents Display Control + * + * Provides an interactive table of contents for document navigation. + * Extracts headings from the document and displays them in a hierarchical + * structure with clickable links for quick navigation. + * + * Features: + * - Automatic heading extraction from document + * - Hierarchical display with proper indentation + * - Clickable navigation links with smooth scrolling + * - Real-time updates when document structure changes + * - Collapsible sections for better organization + * - Search functionality within the table of contents + * + * Dependencies: + * - ControlBase (base control functionality) + */ + +/** + * ContentsControl - Interactive table of contents control + * + * This control scans the document for headings (h1-h6) and presents them + * in a navigable tree structure. Users can click on any heading to jump + * directly to that section with smooth scrolling. + */ +class ContentsControl extends ControlBase { + constructor() { + super(); + + // Configure for contents functionality + this.config = { + icon: 'πŸ“‹', + title: 'Contents', + className: 'contents-control', + defaultContent: 'Loading table of contents...', + ariaLabel: 'Table of Contents Control', + position: 'w' // West positioning + }; + + // Contents-specific state + this.headings = []; + this.lastScanTime = null; + this.updateInterval = null; + this.searchQuery = ''; + } + + /** + * Extract all headings from the document + * Creates a hierarchical structure of the document's heading elements + */ + extractHeadings() { + return this.safeOperation(() => { + const headingSelectors = 'h1, h2, h3, h4, h5, h6'; + const headingElements = document.querySelectorAll(headingSelectors); + const extractedHeadings = []; + + headingElements.forEach((heading, index) => { + const level = parseInt(heading.tagName.charAt(1)); + const text = heading.textContent.trim(); + + // Generate or use existing ID for anchor links + let id = heading.id; + if (!id) { + id = text.toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .substring(0, 50); + + // Ensure uniqueness + let counter = 1; + let uniqueId = id; + while (document.getElementById(uniqueId)) { + uniqueId = `${id}-${counter}`; + counter++; + } + + heading.id = uniqueId; + id = uniqueId; + } + + extractedHeadings.push({ + id, + text, + level, + element: heading, + index + }); + }); + + this.headings = extractedHeadings; + this.lastScanTime = Date.now(); + return extractedHeadings; + + }, [], 'extractHeadings'); + } + + /** + * Filter headings based on search query + */ + filterHeadings(headings, query) { + if (!query || query.trim() === '') { + return headings; + } + + const normalizedQuery = query.toLowerCase().trim(); + return headings.filter(heading => + heading.text.toLowerCase().includes(normalizedQuery) + ); + } + + /** + * Generate HTML for the table of contents + */ + generateContentsHTML(headings = null) { + return this.safeOperation(() => { + const displayHeadings = headings || this.headings; + + if (displayHeadings.length === 0) { + return ` +
+

No headings found in document

+ +
+ `; + } + + const searchHTML = ` +
+ +
+ `; + + const contentsHTML = displayHeadings.map(heading => { + const indentLevel = Math.max(0, heading.level - 1); + const indentPx = indentLevel * 15; + + return ` +
+ + H${heading.level} + ${heading.text} + +
+ `; + }).join(''); + + return ` +
+ ${searchHTML} +
+
+ Found ${displayHeadings.length} heading${displayHeadings.length !== 1 ? 's' : ''} +
+ ${contentsHTML} +
+
+ +
+
+ `; + + }, '

Error generating contents

', 'generateContentsHTML'); + } + + /** + * Navigate to a specific heading with smooth scrolling + */ + navigateToHeading(headingId) { + return this.safeOperation(() => { + const targetElement = document.getElementById(headingId); + if (targetElement) { + targetElement.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + + // Highlight the target temporarily + const originalStyle = targetElement.style.backgroundColor; + targetElement.style.backgroundColor = '#fff3cd'; + targetElement.style.transition = 'background-color 0.3s ease'; + + setTimeout(() => { + targetElement.style.backgroundColor = originalStyle; + setTimeout(() => { + targetElement.style.transition = ''; + }, 300); + }, 1500); + + return true; + } + return false; + }, false, 'navigateToHeading'); + } + + /** + * Handle search input + */ + handleSearch(query) { + this.searchQuery = query; + const filteredHeadings = this.filterHeadings(this.headings, query); + this.updateContentsDisplay(filteredHeadings); + } + + /** + * Update the contents display with new headings + */ + updateContentsDisplay(headings) { + return this.safeOperation(() => { + const content = this.element?.querySelector('.control-content'); + if (content) { + content.innerHTML = this.generateContentsHTML(headings); + } + }, null, 'updateContentsDisplay'); + } + + /** + * Refresh the contents by re-scanning the document + */ + refreshContents() { + return this.safeOperation(() => { + this.extractHeadings(); + const filteredHeadings = this.filterHeadings(this.headings, this.searchQuery); + this.updateContentsDisplay(filteredHeadings); + + // Show success feedback + const refreshBtn = this.element?.querySelector('button'); + if (refreshBtn) { + const originalText = refreshBtn.innerHTML; + refreshBtn.innerHTML = 'βœ… Updated'; + refreshBtn.style.background = '#28a745'; + + setTimeout(() => { + refreshBtn.innerHTML = originalText; + refreshBtn.style.background = '#28a745'; + }, 1000); + } + }, null, 'refreshContents'); + } + + /** + * Build the control content + * Override of base class method to provide contents-specific functionality + */ + buildContent() { + return this.safeOperation(() => { + // Extract headings on first build + this.extractHeadings(); + + // Generate and set content + const content = this.element?.querySelector('.control-content'); + if (content) { + content.innerHTML = this.generateContentsHTML(); + + // Store reference to this control for onclick handlers + this.element.contentsControl = this; + } + + // Set up auto-refresh for dynamic content + if (this.updateInterval) { + clearInterval(this.updateInterval); + } + + this.updateInterval = setInterval(() => { + const currentHeadingCount = document.querySelectorAll('h1, h2, h3, h4, h5, h6').length; + if (currentHeadingCount !== this.headings.length) { + this.refreshContents(); + } + }, 5000); // Check every 5 seconds + + }, null, 'buildContent'); + } + + /** + * Get statistics about the document structure + */ + getDocumentStats() { + return this.safeOperation(() => { + const stats = { + totalHeadings: this.headings.length, + byLevel: {}, + deepestLevel: 1, + structure: 'flat' + }; + + // Count headings by level + this.headings.forEach(heading => { + stats.byLevel[heading.level] = (stats.byLevel[heading.level] || 0) + 1; + stats.deepestLevel = Math.max(stats.deepestLevel, heading.level); + }); + + // Determine structure type + const levels = Object.keys(stats.byLevel).map(Number).sort(); + if (levels.length > 1) { + const hasSequentialLevels = levels.every((level, index) => + index === 0 || level <= levels[index - 1] + 1 + ); + stats.structure = hasSequentialLevels ? 'hierarchical' : 'mixed'; + } + + return stats; + }, {}, 'getDocumentStats'); + } + + /** + * Clean up resources when control is destroyed + */ + destroy() { + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = null; + } + super.destroy(); + } +} + +// Export for module systems or attach to global for direct usage +if (typeof module !== 'undefined' && module.exports) { + module.exports = ContentsControl; +} else { + window.ContentsControl = ContentsControl; +} \ No newline at end of file diff --git a/capabilities/testdrive-jsui/js/controls/control-base.js b/capabilities/testdrive-jsui/js/controls/control-base.js new file mode 100644 index 00000000..0706c4b7 --- /dev/null +++ b/capabilities/testdrive-jsui/js/controls/control-base.js @@ -0,0 +1,631 @@ +/** + * Base Control Class for TestDrive-JSUI Controls + * + * Provides common functionality for positioning, drag, resize, expand/collapse operations. + * This is the foundation class that all UI controls inherit from to ensure consistent + * behavior across the TestDrive-JSUI component system. + * + * Key Features: + * - Drag and drop positioning with compass-based anchoring + * - Resize handles with hover-based visibility + * - Expand/collapse state management + * - Safe operation wrappers with error handling + * - Development mode with strict error checking + * - Accessibility support with proper ARIA labels + * + * Dependencies: + * - None (standalone base class) + * + * Usage: + * Controls inherit from this base by using Object.create(Control) and + * implementing their specific buildContent() methods. + */ + +// Development mode detection for enhanced error reporting +const MARKITECT_STRICT_MODE = ( + window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' || + window.location.search.includes('strict=true') || + window.markitectStrictMode === true +); + +/** + * ControlBase - Foundation class for all TestDrive-JSUI controls + * + * Provides the base functionality that all controls inherit: + * - DOM element management + * - Positioning and drag behavior + * - Resize handle management + * - State persistence + * - Error handling with strict mode support + */ +class ControlBase { + constructor() { + // Default configuration that controls can override + this.config = { + icon: 'πŸ”§', + title: 'Control', + className: 'base-control', + defaultContent: 'Control content', + ariaLabel: 'Base Control', + position: 'w', // Compass position: west (middle-left) + footer: null // Custom footer text + }; + + // Internal state + this.element = null; + this.isExpanded = false; + this.isHeaderOnly = false; // New state for header-only visibility + this.isDragging = false; + this.isResizing = false; + this.position = { x: 0, y: 0 }; + this.size = { width: 300, height: 200 }; + this.originalPosition = null; // Store original position for collapse + + // Event handlers storage + this.eventHandlers = new Map(); + } + + /** + * Safe operation wrapper with error handling + * Provides consistent error handling across all control operations + */ + safeOperation(operation, fallback = null, context = 'Unknown') { + try { + return operation(); + } catch (error) { + console.error(`Control operation failed in ${context}:`, error); + + if (MARKITECT_STRICT_MODE) { + throw error; // Re-throw in strict mode for debugging + } + + return fallback; + } + } + + /** + * Create and initialize the control element + * This method sets up the basic DOM structure that all controls use + */ + createElement() { + return this.safeOperation(() => { + if (this.element) { + this.destroy(); // Clean up existing element + } + + const control = document.createElement('div'); + control.className = `control-panel ${this.config.className}`; + control.setAttribute('role', 'dialog'); + control.setAttribute('aria-label', this.config.ariaLabel); + + control.innerHTML = ` + + + `; + + this.element = control; + this.setupStyles(); + this.setupEventListeners(); + return control; + + }, null, 'createElement'); + } + + /** + * Set up base styles for the control + */ + setupStyles() { + if (!this.element) return; + + // Position the element + this.element.style.position = 'fixed'; + this.element.style.zIndex = '1000'; + + // Store original position for collapse + this.storeOriginalPosition(); + + // Style the icon-only toggle button + const toggleBtn = this.element.querySelector('.control-toggle'); + if (toggleBtn) { + toggleBtn.style.cssText = ` + width: 40px; + height: 40px; + border: none; + background: rgba(248, 249, 250, 0.95); + border: 1px solid #dee2e6; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + transition: all 0.2s ease; + `; + } + } + + /** + * Set up event listeners for control interaction + * Handles dragging, resizing, and toggle functionality + */ + setupEventListeners() { + if (!this.element) return; + + // Icon toggle to expand + const toggleBtn = this.element.querySelector('.control-toggle'); + if (toggleBtn) { + this.addEventListener(toggleBtn, 'click', () => this.expand()); + } + + // Close button to collapse back to icon + const closeBtn = this.element.querySelector('.control-close'); + if (closeBtn) { + this.addEventListener(closeBtn, 'click', () => this.collapse()); + } + + // Header title click to toggle content visibility + const title = this.element.querySelector('.control-title'); + if (title) { + this.addEventListener(title, 'click', () => this.toggleHeaderOnly()); + } + + // Drag functionality on header when expanded + const header = this.element.querySelector('.control-header'); + if (header) { + this.addEventListener(header, 'mousedown', (e) => { + if (this.isExpanded && e.target !== title && e.target !== closeBtn) { + this.startDrag(e); + } + }); + } + } + + /** + * Add event listener with automatic cleanup tracking + */ + addEventListener(element, event, handler) { + const key = `${element.className}_${event}`; + + // Remove existing handler if it exists + if (this.eventHandlers.has(key)) { + const [oldElement, oldEvent, oldHandler] = this.eventHandlers.get(key); + oldElement.removeEventListener(oldEvent, oldHandler); + } + + // Add new handler + element.addEventListener(event, handler); + this.eventHandlers.set(key, [element, event, handler]); + } + + /** + * Store original position for collapse restoration + */ + storeOriginalPosition() { + if (!this.element) return; + + const positionStyles = this.getCompassPosition(); + this.originalPosition = { + top: positionStyles.top, + left: positionStyles.left, + right: positionStyles.right, + bottom: positionStyles.bottom, + transform: positionStyles.transform + }; + + // Apply original position + Object.assign(this.element.style, positionStyles); + } + + /** + * Get compass-based positioning styles + */ + getCompassPosition() { + const positions = { + 'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' }, + 'ne': { top: '20px', right: '20px' }, + 'e': { right: '20px', top: '50%', transform: 'translateY(-50%)' }, + 'se': { bottom: '20px', right: '20px' }, + 's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' }, + 'sw': { bottom: '20px', left: '20px' }, + 'w': { left: '20px', top: '50%', transform: 'translateY(-50%)' }, + 'nw': { top: '20px', left: '20px' } + }; + + return positions[this.config.position] || positions['w']; + } + + /** + * Expand the control from icon-only state + */ + expand() { + return this.safeOperation(() => { + this.isExpanded = true; + const panel = this.element?.querySelector('.control-panel-expanded'); + const toggleBtn = this.element?.querySelector('.control-toggle'); + + if (panel && toggleBtn) { + panel.style.display = 'block'; + toggleBtn.style.display = 'none'; + + // Style expanded panel + panel.style.cssText = ` + background: rgba(248, 249, 250, 0.95); + border: 1px solid #dee2e6; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + backdrop-filter: blur(8px); + min-width: 300px; + min-height: 200px; + `; + + // Style header + const header = this.element.querySelector('.control-header'); + if (header) { + header.style.cssText = ` + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: rgba(0,0,0,0.05); + border-bottom: 1px solid #dee2e6; + cursor: move; + user-select: none; + `; + } + + // Style close button + const closeBtn = this.element.querySelector('.control-close'); + if (closeBtn) { + closeBtn.style.cssText = ` + background: none; + border: none; + font-size: 16px; + cursor: pointer; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + `; + } + + // Add resize handle + this.addResizeHandle(); + this.buildContent(); + } + + return this.isExpanded; + }, false, 'expand'); + } + + /** + * Collapse back to icon-only state at original position + */ + collapse() { + return this.safeOperation(() => { + this.isExpanded = false; + this.isHeaderOnly = false; + const panel = this.element?.querySelector('.control-panel-expanded'); + const toggleBtn = this.element?.querySelector('.control-toggle'); + + if (panel && toggleBtn) { + panel.style.display = 'none'; + toggleBtn.style.display = 'block'; + + // Restore original position + if (this.originalPosition) { + // Clear any drag positioning + this.element.style.left = this.originalPosition.left || ''; + this.element.style.right = this.originalPosition.right || ''; + this.element.style.top = this.originalPosition.top || ''; + this.element.style.bottom = this.originalPosition.bottom || ''; + this.element.style.transform = this.originalPosition.transform || ''; + } + + // Remove resize handle + this.removeResizeHandle(); + } + + return !this.isExpanded; + }, false, 'collapse'); + } + + /** + * Toggle header-only visibility (content show/hide) + */ + toggleHeaderOnly() { + return this.safeOperation(() => { + if (!this.isExpanded) { + // If collapsed, expand first + this.expand(); + return; + } + + const content = this.element?.querySelector('.control-content'); + if (content) { + this.isHeaderOnly = !this.isHeaderOnly; + content.style.display = this.isHeaderOnly ? 'none' : 'block'; + } + + return this.isHeaderOnly; + }, false, 'toggleHeaderOnly'); + } + + /** + * Start drag operation + */ + startDrag(event) { + if (!this.isExpanded) return; // Only drag when expanded + + this.isDragging = true; + const rect = this.element.getBoundingClientRect(); + + // Calculate offset from mouse to element origin + this.dragOffset = { + x: event.clientX - rect.left, + y: event.clientY - rect.top + }; + + // Clear any positioning styles that interfere with dragging + this.element.style.right = ''; + this.element.style.bottom = ''; + this.element.style.transform = ''; + + // Add global mouse move and up handlers + const handleMouseMove = (e) => this.handleDrag(e); + const handleMouseUp = () => this.stopDrag(); + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + + // Store handlers for cleanup (but don't use the tracked version to avoid conflicts) + this._dragHandlers = { move: handleMouseMove, up: handleMouseUp }; + + event.preventDefault(); + } + + /** + * Handle drag movement + */ + handleDrag(event) { + if (!this.isDragging || !this.element) return; + + // Calculate new position based on mouse position and offset + const newX = event.clientX - this.dragOffset.x; + const newY = event.clientY - this.dragOffset.y; + + // Update element position + this.element.style.left = `${newX}px`; + this.element.style.top = `${newY}px`; + + this.position.x = newX; + this.position.y = newY; + + event.preventDefault(); + } + + /** + * Stop drag operation + */ + stopDrag() { + if (!this.isDragging) return; + + this.isDragging = false; + + // Clean up event handlers + if (this._dragHandlers) { + document.removeEventListener('mousemove', this._dragHandlers.move); + document.removeEventListener('mouseup', this._dragHandlers.up); + delete this._dragHandlers; + } + } + + /** + * Add resize handle to expanded control + */ + addResizeHandle() { + // Remove existing resize handle if any + this.removeResizeHandle(); + + const resizeHandle = document.createElement('div'); + resizeHandle.className = 'control-resize-handle'; + resizeHandle.innerHTML = '↙'; // Bottom-left resize indicator + resizeHandle.style.cssText = ` + position: absolute; + bottom: 0; + left: 0; + width: 20px; + height: 20px; + cursor: nw-resize; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0,0,0,0.1); + border-radius: 0 8px 0 0; + user-select: none; + `; + + // Add to the expanded panel + const panel = this.element?.querySelector('.control-panel-expanded'); + if (panel) { + panel.appendChild(resizeHandle); + + // Set up resize handlers + this.addEventListener(resizeHandle, 'mousedown', (e) => this.startResize(e)); + } + } + + /** + * Remove resize handle + */ + removeResizeHandle() { + const handle = this.element?.querySelector('.control-resize-handle'); + if (handle && handle.parentNode) { + handle.parentNode.removeChild(handle); + } + } + + /** + * Start resize operation + */ + startResize(event) { + event.stopPropagation(); // Prevent drag from starting + if (!this.isExpanded) return; + + this.isResizing = true; + const rect = this.element.getBoundingClientRect(); + + // Store initial size and mouse position + this.resizeStart = { + width: rect.width, + height: rect.height, + mouseX: event.clientX, + mouseY: event.clientY + }; + + // Add global mouse move and up handlers + const handleMouseMove = (e) => this.handleResize(e); + const handleMouseUp = () => this.stopResize(); + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + + // Store handlers for cleanup + this._resizeHandlers = { move: handleMouseMove, up: handleMouseUp }; + + event.preventDefault(); + } + + /** + * Handle resize movement (bottom-left corner resize) + */ + handleResize(event) { + if (!this.isResizing || !this.element) return; + + const panel = this.element.querySelector('.control-panel-expanded'); + if (!panel) return; + + // Calculate size change based on mouse movement + const deltaX = this.resizeStart.mouseX - event.clientX; // Inverted for left edge + const deltaY = event.clientY - this.resizeStart.mouseY; + + // Calculate new dimensions (minimum size constraints) + const newWidth = Math.max(200, this.resizeStart.width + deltaX); + const newHeight = Math.max(150, this.resizeStart.height + deltaY); + + // Apply new size to the panel + panel.style.width = `${newWidth}px`; + panel.style.height = `${newHeight}px`; + + // Update stored size + this.size.width = newWidth; + this.size.height = newHeight; + + event.preventDefault(); + } + + /** + * Stop resize operation + */ + stopResize() { + if (!this.isResizing) return; + + this.isResizing = false; + + // Clean up event handlers + if (this._resizeHandlers) { + document.removeEventListener('mousemove', this._resizeHandlers.move); + document.removeEventListener('mouseup', this._resizeHandlers.up); + delete this._resizeHandlers; + } + } + + /** + * Position the control based on compass position (used by show method) + */ + positionControl() { + if (!this.element) return; + + // Use the compass positioning from setupStyles + this.storeOriginalPosition(); + } + + /** + * Build the control content (to be overridden by subclasses) + */ + buildContent() { + // Default implementation - subclasses should override this + const content = this.element?.querySelector('.control-content'); + if (content) { + content.innerHTML = this.config.defaultContent; + } + } + + /** + * Show the control + */ + show() { + return this.safeOperation(() => { + if (!this.element) { + this.createElement(); + } + + document.body.appendChild(this.element); + this.positionControl(); + this.buildContent(); + + return this.element; + }, null, 'show'); + } + + /** + * Hide the control + */ + hide() { + return this.safeOperation(() => { + if (this.element && this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + }, null, 'hide'); + } + + /** + * Destroy the control and clean up resources + */ + destroy() { + return this.safeOperation(() => { + // Clean up event listeners + for (const [element, event, handler] of this.eventHandlers.values()) { + element.removeEventListener(event, handler); + } + this.eventHandlers.clear(); + + // Remove element from DOM + if (this.element && this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + + this.element = null; + }, null, 'destroy'); + } +} + +// Export for module systems or attach to global for direct usage +if (typeof module !== 'undefined' && module.exports) { + module.exports = ControlBase; +} else { + window.ControlBase = ControlBase; +} \ No newline at end of file diff --git a/capabilities/testdrive-jsui/js/controls/debug-control.js b/capabilities/testdrive-jsui/js/controls/debug-control.js new file mode 100644 index 00000000..592ddea8 --- /dev/null +++ b/capabilities/testdrive-jsui/js/controls/debug-control.js @@ -0,0 +1,479 @@ +/** + * DebugControl - System Debug Information and Message Display Control + * + * Provides comprehensive debugging capabilities including system message display, + * error tracking, performance monitoring, and development tools. Essential for + * troubleshooting and development workflows within the TestDrive-JSUI environment. + * + * Features: + * - Real-time debug message display with categorization + * - Error tracking with stack trace information + * - Performance metrics and timing measurements + * - System information display (browser, viewport, etc.) + * - Message filtering and search capabilities + * - Export functionality for debug logs + * - Integration with MarkitectDebugSystem + * + * Dependencies: + * - ControlBase (base control functionality) + * - MarkitectDebugSystem (optional, for enhanced debugging) + */ + +/** + * DebugControl - Development and debugging information control + * + * This control serves as a central hub for all debugging activities, + * providing developers with essential information for troubleshooting + * and performance optimization. + */ +class DebugControl extends ControlBase { + constructor() { + super(); + + // Configure for debug functionality + this.config = { + icon: 'πŸ›', + title: 'Debug', + className: 'debug-control', + defaultContent: 'Debug information loading...', + ariaLabel: 'Debug Information Control', + position: 'w' // West positioning + }; + + // Debug control state + this.messages = []; + this.maxMessages = 100; + this.messageFilter = 'all'; // 'all', 'error', 'warn', 'info', 'debug' + this.autoScroll = true; + this.isRecording = true; + this.startTime = Date.now(); + this.performanceMarks = new Map(); + + this.initializeDebugCapture(); + } + + /** + * Initialize debug message capture + */ + initializeDebugCapture() { + return this.safeOperation(() => { + // Capture console messages + this.originalConsole = { + log: console.log, + error: console.error, + warn: console.warn, + info: console.info, + debug: console.debug + }; + + // Override console methods to capture messages + console.log = (...args) => { + this.originalConsole.log(...args); + this.addDebugMessage('LOG', args.join(' '), 'info'); + }; + + console.error = (...args) => { + this.originalConsole.error(...args); + this.addDebugMessage('ERROR', args.join(' '), 'error'); + }; + + console.warn = (...args) => { + this.originalConsole.warn(...args); + this.addDebugMessage('WARN', args.join(' '), 'warn'); + }; + + console.info = (...args) => { + this.originalConsole.info(...args); + this.addDebugMessage('INFO', args.join(' '), 'info'); + }; + + console.debug = (...args) => { + this.originalConsole.debug(...args); + this.addDebugMessage('DEBUG', args.join(' '), 'debug'); + }; + + // Capture global errors + window.addEventListener('error', (event) => { + this.addDebugMessage('ERROR', `${event.message} at ${event.filename}:${event.lineno}`, 'error'); + }); + + // Capture unhandled promise rejections + window.addEventListener('unhandledrejection', (event) => { + this.addDebugMessage('PROMISE_REJECT', `Unhandled promise rejection: ${event.reason}`, 'error'); + }); + + }, null, 'initializeDebugCapture'); + } + + /** + * Add a debug message to the log + */ + addDebugMessage(category, message, level = 'info') { + return this.safeOperation(() => { + if (!this.isRecording) return; + + const debugMessage = { + id: Date.now() + Math.random(), + timestamp: Date.now(), + category, + message, + level, + displayTime: new Date().toLocaleTimeString(), + relativeTime: Date.now() - this.startTime + }; + + this.messages.push(debugMessage); + + // Limit message history + if (this.messages.length > this.maxMessages) { + this.messages.shift(); + } + + // Update display if visible + if (this.element && this.isExpanded) { + this.updateMessageDisplay(); + } + + }, null, 'addDebugMessage'); + } + + /** + * Get messages filtered by current filter setting + */ + getFilteredMessages() { + if (this.messageFilter === 'all') { + return this.messages; + } + return this.messages.filter(msg => msg.level === this.messageFilter); + } + + /** + * Generate system information HTML + */ + generateSystemInfoHTML() { + return this.safeOperation(() => { + const systemInfo = { + userAgent: navigator.userAgent, + viewport: `${window.innerWidth}x${window.innerHeight}`, + screen: `${screen.width}x${screen.height}`, + colorDepth: screen.colorDepth, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + language: navigator.language, + cookieEnabled: navigator.cookieEnabled, + onlineStatus: navigator.onLine ? 'Online' : 'Offline', + protocol: window.location.protocol, + memory: performance.memory ? + `Used: ${Math.round(performance.memory.usedJSHeapSize / 1024 / 1024)}MB` : + 'Not available' + }; + + return ` +
+ System Information:
+
+
Viewport: ${systemInfo.viewport}
+
Screen: ${systemInfo.screen}
+
Memory: ${systemInfo.memory}
+
Language: ${systemInfo.language}
+
Status: ${systemInfo.onlineStatus}
+
Protocol: ${systemInfo.protocol}
+
+
+ `; + + }, '', 'generateSystemInfoHTML'); + } + + /** + * Generate performance metrics HTML + */ + generatePerformanceHTML() { + return this.safeOperation(() => { + const timing = performance.timing; + const navigation = performance.getEntriesByType('navigation')[0]; + + const metrics = { + pageLoad: timing.loadEventEnd - timing.navigationStart, + domReady: timing.domContentLoadedEventEnd - timing.navigationStart, + firstByte: timing.responseStart - timing.navigationStart, + uptime: Date.now() - this.startTime, + messagesCount: this.messages.length + }; + + return ` +
+ Performance Metrics:
+
+
Page Load: ${metrics.pageLoad}ms
+
DOM Ready: ${metrics.domReady}ms
+
First Byte: ${metrics.firstByte}ms
+
Session Time: ${Math.round(metrics.uptime / 1000)}s
+
Debug Messages: ${metrics.messagesCount}
+
+
+ `; + + }, '', 'generatePerformanceHTML'); + } + + /** + * Generate debug messages HTML + */ + generateMessagesHTML() { + return this.safeOperation(() => { + const filteredMessages = this.getFilteredMessages(); + + if (filteredMessages.length === 0) { + return ` +
+ No ${this.messageFilter === 'all' ? '' : this.messageFilter + ' '}messages yet +
+ `; + } + + const messagesHTML = filteredMessages.slice(-20).map(msg => { + const levelColors = { + error: '#dc3545', + warn: '#ffc107', + info: '#17a2b8', + debug: '#6c757d' + }; + + const backgroundColor = levelColors[msg.level] || '#6c757d'; + const textColor = msg.level === 'warn' ? '#000' : '#fff'; + + return ` +
+
+ + ${msg.category} + + + ${msg.displayTime} + +
+
+ ${msg.message} +
+
+ `; + }).join(''); + + return ` +
+ ${messagesHTML} +
+ `; + + }, '

Error displaying messages

', 'generateMessagesHTML'); + } + + /** + * Generate control buttons HTML + */ + generateControlButtonsHTML() { + return ` +
+ + + + + + + +
+ `; + } + + /** + * Generate filter controls HTML + */ + generateFilterControlsHTML() { + const filters = ['all', 'error', 'warn', 'info', 'debug']; + + const filterButtons = filters.map(filter => { + const isActive = this.messageFilter === filter; + return ` + + `; + }).join(''); + + return ` +
+
Filter:
+ ${filterButtons} +
+ `; + } + + /** + * Update the message display + */ + updateMessageDisplay() { + return this.safeOperation(() => { + const messagesContainer = this.element?.querySelector('.messages-container'); + if (messagesContainer) { + const parent = messagesContainer.parentElement; + parent.innerHTML = this.generateMessagesHTML(); + + // Auto-scroll to bottom if enabled + if (this.autoScroll) { + const newContainer = parent.querySelector('.messages-container'); + if (newContainer) { + newContainer.scrollTop = newContainer.scrollHeight; + } + } + } + }, null, 'updateMessageDisplay'); + } + + /** + * Clear all debug messages + */ + clearMessages() { + this.messages = []; + if (window.MarkitectDebugSystem) { + window.MarkitectDebugSystem.clearMessages(); + } + this.buildContent(); + } + + /** + * Export debug messages to file + */ + exportMessages() { + return this.safeOperation(() => { + const exportData = { + timestamp: new Date().toISOString(), + session: { + startTime: new Date(this.startTime).toISOString(), + duration: Date.now() - this.startTime, + messageCount: this.messages.length + }, + system: { + userAgent: navigator.userAgent, + viewport: `${window.innerWidth}x${window.innerHeight}`, + url: window.location.href + }, + messages: this.messages + }; + + const dataStr = JSON.stringify(exportData, null, 2); + const dataBlob = new Blob([dataStr], { type: 'application/json' }); + const url = URL.createObjectURL(dataBlob); + + const link = document.createElement('a'); + link.href = url; + link.download = `debug-log-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + URL.revokeObjectURL(url); + this.addDebugMessage('EXPORT', 'Debug log exported successfully', 'info'); + + }, null, 'exportMessages'); + } + + /** + * Toggle message recording + */ + toggleRecording() { + this.isRecording = !this.isRecording; + this.buildContent(); + this.addDebugMessage('CONTROL', `Recording ${this.isRecording ? 'started' : 'paused'}`, 'info'); + } + + /** + * Add a test message + */ + addTestMessage() { + const testMessages = [ + { category: 'TEST', message: 'This is a test info message', level: 'info' }, + { category: 'TEST', message: 'This is a test warning message', level: 'warn' }, + { category: 'TEST', message: 'This is a test error message', level: 'error' }, + { category: 'TEST', message: 'This is a test debug message', level: 'debug' } + ]; + + const randomMessage = testMessages[Math.floor(Math.random() * testMessages.length)]; + this.addDebugMessage(randomMessage.category, randomMessage.message, randomMessage.level); + } + + /** + * Set message filter + */ + setMessageFilter(filter) { + this.messageFilter = filter; + this.buildContent(); + } + + /** + * Build the control content + * Override of base class method to provide debug-specific functionality + */ + buildContent() { + return this.safeOperation(() => { + const content = this.element?.querySelector('.control-content'); + if (content) { + content.innerHTML = ` +
+

Debug Information

+ + ${this.generateSystemInfoHTML()} + ${this.generatePerformanceHTML()} + ${this.generateFilterControlsHTML()} + ${this.generateMessagesHTML()} + ${this.generateControlButtonsHTML()} + +
+ Recording: ${this.isRecording ? '🟒 Active' : 'πŸ”΄ Paused'} | + Filter: ${this.messageFilter.toUpperCase()} | + Messages: ${this.getFilteredMessages().length}/${this.messages.length} +
+
+ `; + + // Store reference to this control for onclick handlers + this.element.debugControl = this; + } + }, null, 'buildContent'); + } + + /** + * Clean up resources when control is destroyed + */ + destroy() { + // Restore original console methods + if (this.originalConsole) { + console.log = this.originalConsole.log; + console.error = this.originalConsole.error; + console.warn = this.originalConsole.warn; + console.info = this.originalConsole.info; + console.debug = this.originalConsole.debug; + } + + super.destroy(); + } +} + +// Export for module systems or attach to global for direct usage +if (typeof module !== 'undefined' && module.exports) { + module.exports = DebugControl; +} else { + window.DebugControl = DebugControl; +} \ No newline at end of file diff --git a/capabilities/testdrive-jsui/js/controls/edit-control.js b/capabilities/testdrive-jsui/js/controls/edit-control.js new file mode 100644 index 00000000..7a20a7d2 --- /dev/null +++ b/capabilities/testdrive-jsui/js/controls/edit-control.js @@ -0,0 +1,500 @@ +/** + * EditControl - Document Editing Tools and Actions Control + * + * Provides a comprehensive set of document editing tools including text formatting, + * document actions (print, save, export), navigation helpers, and editing modes. + * Designed to enhance the writing and editing experience within the TestDrive-JSUI + * environment. + * + * Features: + * - Document actions (print, save, export to various formats) + * - Text formatting tools (bold, italic, headers) + * - Navigation helpers (scroll to top/bottom, go to line) + * - Word processing features (find/replace, word count) + * - Accessibility tools (font size, contrast adjustment) + * - Markdown formatting shortcuts + * + * Dependencies: + * - ControlBase (base control functionality) + */ + +/** + * EditControl - Comprehensive document editing control + * + * This control provides writers and editors with essential tools for document + * creation and modification. It includes both basic text operations and + * advanced features for content management and formatting. + */ +class EditControl extends ControlBase { + constructor() { + super(); + + // Configure for editing functionality + this.config = { + icon: '✏️', + title: 'Edit', + className: 'edit-control', + defaultContent: 'Document editing tools loading...', + ariaLabel: 'Document Edit Control', + position: 'e' // East positioning + }; + + // Edit control state + this.editingMode = 'view'; // 'view', 'edit', 'preview' + this.fontSize = 16; + this.lastSaveTime = null; + this.unsavedChanges = false; + this.shortcuts = new Map(); + + this.initializeShortcuts(); + } + + /** + * Initialize keyboard shortcuts for editing + */ + initializeShortcuts() { + this.shortcuts.set('Ctrl+S', () => this.saveDocument()); + this.shortcuts.set('Ctrl+P', () => this.printDocument()); + this.shortcuts.set('Ctrl+F', () => this.showFindDialog()); + this.shortcuts.set('Ctrl+B', () => this.toggleBold()); + this.shortcuts.set('Ctrl+I', () => this.toggleItalic()); + this.shortcuts.set('Escape', () => this.exitEditMode()); + } + + /** + * Generate the main editing tools HTML + */ + generateEditToolsHTML() { + return this.safeOperation(() => { + return ` +
+

Edit Tools

+ + +
+
Document Actions
+ + + + + + +
+ + + + + +
+
Text Tools
+ + + +
+ + + +
+ + +
+ + +
+
Markdown Tools
+ +
+ + + + + + + +
+
+ + +
+
+
Mode: ${this.editingMode}
+
Font: ${this.fontSize}px
+ ${this.lastSaveTime ? `
Saved: ${new Date(this.lastSaveTime).toLocaleTimeString()}
` : ''} + ${this.unsavedChanges ? '
⚠️ Unsaved changes
' : ''} +
+
+
+ `; + + }, '

Error generating edit tools

', 'generateEditToolsHTML'); + } + + /** + * Print the document + */ + printDocument() { + return this.safeOperation(() => { + window.print(); + + // Show feedback + this.showActionFeedback('πŸ–¨οΈ Print dialog opened', '#28a745'); + }, null, 'printDocument'); + } + + /** + * Save document (placeholder - would integrate with actual save system) + */ + saveDocument() { + return this.safeOperation(() => { + // In a real implementation, this would save to a backend + this.lastSaveTime = Date.now(); + this.unsavedChanges = false; + + // Update display + this.buildContent(); + + // Show feedback + this.showActionFeedback('πŸ’Ύ Document saved', '#007bff'); + }, null, 'saveDocument'); + } + + /** + * Export document to various formats + */ + exportDocument() { + return this.safeOperation(() => { + const contentArea = document.querySelector('#markitect-content') || document.body; + const htmlContent = contentArea.innerHTML; + const textContent = contentArea.textContent; + + // Create export menu + const exportMenu = document.createElement('div'); + exportMenu.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + border: 2px solid #007bff; + border-radius: 8px; + padding: 1rem; + z-index: 10000; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + `; + + exportMenu.innerHTML = ` +

Export Document

+ + + + + `; + + // Add export functions + exportMenu.exportAsHTML = () => { + this.downloadFile(htmlContent, 'document.html', 'text/html'); + document.body.removeChild(exportMenu); + }; + + exportMenu.exportAsText = () => { + this.downloadFile(textContent, 'document.txt', 'text/plain'); + document.body.removeChild(exportMenu); + }; + + exportMenu.exportAsMarkdown = () => { + // Simple HTML to Markdown conversion (basic) + let markdown = htmlContent + .replace(/]*>(.*?)<\/h1>/gi, '# $1\n\n') + .replace(/]*>(.*?)<\/h2>/gi, '## $1\n\n') + .replace(/]*>(.*?)<\/h3>/gi, '### $1\n\n') + .replace(/]*>(.*?)<\/p>/gi, '$1\n\n') + .replace(/]*>(.*?)<\/strong>/gi, '**$1**') + .replace(/]*>(.*?)<\/em>/gi, '*$1*') + .replace(/<[^>]*>/g, ''); // Remove remaining HTML tags + + this.downloadFile(markdown, 'document.md', 'text/markdown'); + document.body.removeChild(exportMenu); + }; + + document.body.appendChild(exportMenu); + + }, null, 'exportDocument'); + } + + /** + * Download a file with given content + */ + downloadFile(content, filename, mimeType) { + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } + + /** + * Scroll to top of document + */ + scrollToTop() { + window.scrollTo({ top: 0, behavior: 'smooth' }); + this.showActionFeedback('⬆️ Scrolled to top', '#6c757d'); + } + + /** + * Scroll to bottom of document + */ + scrollToBottom() { + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); + this.showActionFeedback('⬇️ Scrolled to bottom', '#6c757d'); + } + + /** + * Show go to line dialog + */ + showGoToLine() { + const lineNumber = prompt('Go to line number:'); + if (lineNumber && !isNaN(lineNumber)) { + // Simple implementation - scroll to approximate position + const totalHeight = document.body.scrollHeight; + const approximatePosition = (parseInt(lineNumber) / 100) * totalHeight; + window.scrollTo({ top: approximatePosition, behavior: 'smooth' }); + this.showActionFeedback(`🎯 Went to line ${lineNumber}`, '#6c757d'); + } + } + + /** + * Show find and replace dialog + */ + showFindReplace() { + const searchTerm = prompt('Find text:'); + if (searchTerm) { + // Simple highlight implementation + this.highlightText(searchTerm); + this.showActionFeedback(`πŸ” Highlighted "${searchTerm}"`, '#ffc107', '#000'); + } + } + + /** + * Highlight text in the document + */ + highlightText(searchTerm) { + return this.safeOperation(() => { + // Remove previous highlights + document.querySelectorAll('.edit-highlight').forEach(el => { + el.outerHTML = el.innerHTML; + }); + + // Add new highlights + const contentArea = document.querySelector('#markitect-content') || document.body; + const walker = document.createTreeWalker( + contentArea, + NodeFilter.SHOW_TEXT, + null, + false + ); + + const textNodes = []; + let node; + while (node = walker.nextNode()) { + textNodes.push(node); + } + + textNodes.forEach(textNode => { + const parent = textNode.parentNode; + const text = textNode.textContent; + if (text.toLowerCase().includes(searchTerm.toLowerCase())) { + const regex = new RegExp(`(${searchTerm})`, 'gi'); + const highlightedHTML = text.replace(regex, '$1'); + + const wrapper = document.createElement('div'); + wrapper.innerHTML = highlightedHTML; + while (wrapper.firstChild) { + parent.insertBefore(wrapper.firstChild, textNode); + } + parent.removeChild(textNode); + } + }); + }, null, 'highlightText'); + } + + /** + * Increase font size + */ + increaseFontSize() { + this.fontSize = Math.min(this.fontSize + 2, 24); + this.applyFontSize(); + this.buildContent(); + } + + /** + * Decrease font size + */ + decreaseFontSize() { + this.fontSize = Math.max(this.fontSize - 2, 12); + this.applyFontSize(); + this.buildContent(); + } + + /** + * Apply font size to document + */ + applyFontSize() { + const contentArea = document.querySelector('#markitect-content') || document.body; + contentArea.style.fontSize = `${this.fontSize}px`; + } + + /** + * Copy page link to clipboard + */ + copyLink() { + return this.safeOperation(() => { + const url = window.location.href; + if (navigator.clipboard) { + navigator.clipboard.writeText(url).then(() => { + this.showActionFeedback('πŸ“‹ Link copied to clipboard', '#fd7e14'); + }); + } else { + // Fallback for older browsers + prompt('Copy this link:', url); + this.showActionFeedback('πŸ“‹ Link displayed for copying', '#fd7e14'); + } + }, null, 'copyLink'); + } + + /** + * Insert markdown formatting + */ + insertMarkdown(prefix, suffix, placeholder) { + // This would integrate with an actual text editor + // For now, just show what would be inserted + const text = `${prefix}${placeholder}${suffix}`; + if (navigator.clipboard) { + navigator.clipboard.writeText(text); + this.showActionFeedback(`πŸ“‹ Copied: ${text}`, '#495057'); + } else { + prompt('Markdown to copy:', text); + } + } + + /** + * Show action feedback message + */ + showActionFeedback(message, backgroundColor, color = 'white') { + const feedback = document.createElement('div'); + feedback.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: ${backgroundColor}; + color: ${color}; + padding: 0.5rem 1rem; + border-radius: 4px; + z-index: 9999; + font-size: 0.8rem; + box-shadow: 0 2px 10px rgba(0,0,0,0.2); + `; + feedback.textContent = message; + document.body.appendChild(feedback); + + setTimeout(() => { + if (feedback.parentNode) { + document.body.removeChild(feedback); + } + }, 3000); + } + + /** + * Build the control content + * Override of base class method to provide edit-specific functionality + */ + buildContent() { + return this.safeOperation(() => { + const content = this.element?.querySelector('.control-content'); + if (content) { + content.innerHTML = this.generateEditToolsHTML(); + + // Store reference to this control for onclick handlers + this.element.editControl = this; + } + }, null, 'buildContent'); + } + + /** + * Exit edit mode + */ + exitEditMode() { + this.editingMode = 'view'; + this.buildContent(); + } +} + +// Export for module systems or attach to global for direct usage +if (typeof module !== 'undefined' && module.exports) { + module.exports = EditControl; +} else { + window.EditControl = EditControl; +} \ No newline at end of file diff --git a/capabilities/testdrive-jsui/js/controls/status-control.js b/capabilities/testdrive-jsui/js/controls/status-control.js new file mode 100644 index 00000000..f3ca19d2 --- /dev/null +++ b/capabilities/testdrive-jsui/js/controls/status-control.js @@ -0,0 +1,368 @@ +/** + * StatusControl - Document Statistics and Change Tracking Control + * + * Provides real-time document statistics including word count, character count, + * reading time estimation, and change tracking. Monitors document modifications + * and provides insights into document structure and content metrics. + * + * Features: + * - Real-time word and character counting + * - Reading time estimation based on content + * - Document structure analysis (headings, paragraphs, lists) + * - Change tracking with before/after comparisons + * - Content complexity metrics + * - Export functionality for statistics + * + * Dependencies: + * - ControlBase (base control functionality) + */ + +/** + * StatusControl - Document statistics and monitoring control + * + * This control continuously monitors the document for changes and provides + * detailed statistics about content, structure, and reading metrics. + * Useful for writers, editors, and content creators. + */ +class StatusControl extends ControlBase { + constructor() { + super(); + + // Configure for status functionality + this.config = { + icon: 'πŸ“Š', + title: 'Status', + className: 'status-control', + defaultContent: 'Loading document statistics...', + ariaLabel: 'Document Status Control', + position: 'e' // East positioning + }; + + // Status tracking state + this.stats = { + characters: 0, + charactersNoSpaces: 0, + words: 0, + sentences: 0, + paragraphs: 0, + headings: 0, + lists: 0, + images: 0, + links: 0, + readingTimeMinutes: 0 + }; + + this.previousStats = { ...this.stats }; + this.lastUpdateTime = null; + this.updateInterval = null; + this.wordsPerMinute = 200; // Average reading speed + } + + /** + * Extract and count document content statistics + */ + analyzeDocument() { + return this.safeOperation(() => { + const contentArea = document.querySelector('#markitect-content') || document.body; + const textContent = contentArea.textContent || ''; + + // Basic text statistics + this.stats.characters = textContent.length; + this.stats.charactersNoSpaces = textContent.replace(/\s/g, '').length; + + // Word counting (more accurate) + const words = textContent.trim().split(/\s+/).filter(word => word.length > 0); + this.stats.words = words.length; + + // Sentence counting (approximate) + const sentences = textContent.split(/[.!?]+/).filter(s => s.trim().length > 0); + this.stats.sentences = sentences.length; + + // Structural elements + this.stats.paragraphs = contentArea.querySelectorAll('p').length; + this.stats.headings = contentArea.querySelectorAll('h1, h2, h3, h4, h5, h6').length; + this.stats.lists = contentArea.querySelectorAll('ul, ol').length; + this.stats.images = contentArea.querySelectorAll('img').length; + this.stats.links = contentArea.querySelectorAll('a').length; + + // Reading time calculation + this.stats.readingTimeMinutes = Math.ceil(this.stats.words / this.wordsPerMinute); + + this.lastUpdateTime = Date.now(); + return this.stats; + + }, this.stats, 'analyzeDocument'); + } + + /** + * Calculate changes since last analysis + */ + calculateChanges() { + return this.safeOperation(() => { + const changes = {}; + for (const [key, currentValue] of Object.entries(this.stats)) { + const previousValue = this.previousStats[key] || 0; + const difference = currentValue - previousValue; + changes[key] = { + current: currentValue, + previous: previousValue, + change: difference, + hasChanged: difference !== 0 + }; + } + return changes; + }, {}, 'calculateChanges'); + } + + /** + * Format statistics for display + */ + formatStatistics() { + return this.safeOperation(() => { + const changes = this.calculateChanges(); + + const formatChange = (changeData) => { + if (!changeData.hasChanged) return ''; + const sign = changeData.change > 0 ? '+' : ''; + const color = changeData.change > 0 ? '#28a745' : '#dc3545'; + return ` (${sign}${changeData.change})`; + }; + + const formatNumber = (num) => num.toLocaleString(); + + return ` +
+

Document Statistics

+ +
+
+ Words:
+ ${formatNumber(this.stats.words)} + ${formatChange(changes.words)} +
+ +
+ Characters:
+ ${formatNumber(this.stats.characters)} + ${formatChange(changes.characters)} +
+ +
+ Reading Time:
+ ${this.stats.readingTimeMinutes} min + ${formatChange(changes.readingTimeMinutes)} +
+ +
+ Sentences:
+ ${formatNumber(this.stats.sentences)} + ${formatChange(changes.sentences)} +
+
+ +
+
Document Structure
+ +
+ Paragraphs: + ${this.stats.paragraphs}${formatChange(changes.paragraphs)} +
+ +
+ Headings: + ${this.stats.headings}${formatChange(changes.headings)} +
+ +
+ Lists: + ${this.stats.lists}${formatChange(changes.lists)} +
+ +
+ Images: + ${this.stats.images}${formatChange(changes.images)} +
+ +
+ Links: + ${this.stats.links}${formatChange(changes.links)} +
+
+ +
+ + + +
+ + ${this.lastUpdateTime ? ` +
+ Updated: ${new Date(this.lastUpdateTime).toLocaleTimeString()} +
+ ` : ''} +
+ `; + + }, '

Error displaying statistics

', 'formatStatistics'); + } + + /** + * Refresh statistics and update display + */ + refreshStats() { + return this.safeOperation(() => { + // Save current stats as previous + this.previousStats = { ...this.stats }; + + // Analyze document + this.analyzeDocument(); + + // Update display + this.buildContent(); + + // Show success feedback + const refreshBtn = this.element?.querySelector('button'); + if (refreshBtn) { + const originalText = refreshBtn.innerHTML; + refreshBtn.innerHTML = 'βœ… Updated'; + + setTimeout(() => { + refreshBtn.innerHTML = originalText; + }, 1000); + } + + }, null, 'refreshStats'); + } + + /** + * Export statistics to various formats + */ + exportStats() { + return this.safeOperation(() => { + const exportData = { + timestamp: new Date().toISOString(), + document: { + title: document.title || 'Untitled Document', + url: window.location.href + }, + statistics: this.stats, + metadata: { + wordsPerMinute: this.wordsPerMinute, + analysisDate: new Date(this.lastUpdateTime).toISOString() + } + }; + + // Create downloadable JSON + const dataStr = JSON.stringify(exportData, null, 2); + const dataBlob = new Blob([dataStr], { type: 'application/json' }); + const url = URL.createObjectURL(dataBlob); + + // Create temporary download link + const link = document.createElement('a'); + link.href = url; + link.download = `document-stats-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Clean up + URL.revokeObjectURL(url); + + // Show feedback + const exportBtn = this.element?.querySelector('button:last-child'); + if (exportBtn) { + const originalText = exportBtn.innerHTML; + exportBtn.innerHTML = 'βœ… Exported'; + exportBtn.style.background = '#28a745'; + + setTimeout(() => { + exportBtn.innerHTML = originalText; + exportBtn.style.background = '#28a745'; + }, 2000); + } + + }, null, 'exportStats'); + } + + /** + * Get reading difficulty score (Flesch Reading Ease approximation) + */ + calculateReadabilityScore() { + return this.safeOperation(() => { + if (this.stats.sentences === 0 || this.stats.words === 0) { + return { score: 0, level: 'Unknown' }; + } + + const avgWordsPerSentence = this.stats.words / this.stats.sentences; + const avgSyllablesPerWord = 1.5; // Simplified approximation + + // Flesch Reading Ease formula (simplified) + const score = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllablesPerWord); + + let level; + if (score >= 90) level = 'Very Easy'; + else if (score >= 80) level = 'Easy'; + else if (score >= 70) level = 'Fairly Easy'; + else if (score >= 60) level = 'Standard'; + else if (score >= 50) level = 'Fairly Difficult'; + else if (score >= 30) level = 'Difficult'; + else level = 'Very Difficult'; + + return { score: Math.round(score), level }; + }, { score: 0, level: 'Unknown' }, 'calculateReadabilityScore'); + } + + /** + * Build the control content + * Override of base class method to provide status-specific functionality + */ + buildContent() { + return this.safeOperation(() => { + // Analyze document first + this.analyzeDocument(); + + // Generate and set content + const content = this.element?.querySelector('.control-content'); + if (content) { + content.innerHTML = this.formatStatistics(); + + // Store reference to this control for onclick handlers + this.element.statusControl = this; + } + + // Set up auto-refresh for dynamic content + if (this.updateInterval) { + clearInterval(this.updateInterval); + } + + this.updateInterval = setInterval(() => { + this.refreshStats(); + }, 10000); // Update every 10 seconds + + }, null, 'buildContent'); + } + + /** + * Clean up resources when control is destroyed + */ + destroy() { + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = null; + } + super.destroy(); + } +} + +// Export for module systems or attach to global for direct usage +if (typeof module !== 'undefined' && module.exports) { + module.exports = StatusControl; +} else { + window.StatusControl = StatusControl; +} \ No newline at end of file diff --git a/capabilities/testdrive-jsui/js/tests/button-events.test.js b/capabilities/testdrive-jsui/js/tests/button-events.test.js index 9d355438..bf6e45fc 100644 --- a/capabilities/testdrive-jsui/js/tests/button-events.test.js +++ b/capabilities/testdrive-jsui/js/tests/button-events.test.js @@ -33,10 +33,10 @@ describe('Button Functionality and DOM Events', () => { mockSection = document.querySelector('.section'); - // Load components - require('../components/document-controls.js'); - if (global.DocumentControls) { - documentControls = new global.DocumentControls(document.getElementById('content')); + // Load components - using legacy component for backward compatibility + require('../components/document-controls-legacy.js'); + if (global.DocumentControlsLegacy) { + documentControls = new global.DocumentControlsLegacy(document.getElementById('content')); } }); diff --git a/capabilities/testdrive-jsui/js/tests/test-documentcontrols-extraction.js b/capabilities/testdrive-jsui/js/tests/test-documentcontrols-extraction.js index 2d5607ca..764313c3 100644 --- a/capabilities/testdrive-jsui/js/tests/test-documentcontrols-extraction.js +++ b/capabilities/testdrive-jsui/js/tests/test-documentcontrols-extraction.js @@ -37,14 +37,14 @@ runner.describe('DocumentControls Component Extraction', () => { runner.it('should load extracted DocumentControls component', () => { // Load the extracted component - delete require.cache[require.resolve('../components/document-controls.js')]; + delete require.cache[require.resolve('../components/document-controls-legacy.js')]; try { - const module = require('../components/document-controls.js'); - runner.expect(module.DocumentControls).toBeTruthy(); + const module = require('../components/document-controls-legacy.js'); + runner.expect(module.DocumentControlsLegacy).toBeTruthy(); // Set global for other tests - global.ExtractedDocumentControls = module.DocumentControls; + global.ExtractedDocumentControls = module.DocumentControlsLegacy; } catch (error) { throw new Error(`Failed to load extracted DocumentControls: ${error.message}`); } diff --git a/capabilities/testdrive-jsui/js/tests/test-full-integration.js b/capabilities/testdrive-jsui/js/tests/test-full-integration.js index 3edb0ced..3b06d123 100644 --- a/capabilities/testdrive-jsui/js/tests/test-full-integration.js +++ b/capabilities/testdrive-jsui/js/tests/test-full-integration.js @@ -19,18 +19,18 @@ runner.describe('Full Component Integration Tests', () => { const sectionModule = require('../core/section-manager.js'); const domModule = require('../components/dom-renderer.js'); const debugModule = require('../components/debug-panel.js'); - const controlsModule = require('../components/document-controls.js'); + const controlsModule = require('../components/document-controls-legacy.js'); runner.expect(sectionModule.SectionManager).toBeTruthy(); runner.expect(domModule.DOMRenderer).toBeTruthy(); runner.expect(debugModule.DebugPanel).toBeTruthy(); - runner.expect(controlsModule.DocumentControls).toBeTruthy(); + runner.expect(controlsModule.DocumentControlsLegacy).toBeTruthy(); // Set globals for other tests global.ExtractedSectionManager = sectionModule.SectionManager; global.ExtractedDOMRenderer = domModule.DOMRenderer; global.ExtractedDebugPanel = debugModule.DebugPanel; - global.ExtractedDocumentControls = controlsModule.DocumentControls; + global.ExtractedDocumentControls = controlsModule.DocumentControlsLegacy; } catch (error) { throw new Error(`Failed to load extracted components: ${error.message}`); diff --git a/capabilities/testdrive-jsui/js/tests/test-real-user-functionality.js b/capabilities/testdrive-jsui/js/tests/test-real-user-functionality.js index 3d7fddef..c5117bec 100644 --- a/capabilities/testdrive-jsui/js/tests/test-real-user-functionality.js +++ b/capabilities/testdrive-jsui/js/tests/test-real-user-functionality.js @@ -18,12 +18,12 @@ runner.describe('Real User Functionality Tests', () => { const sectionModule = require('../core/section-manager.js'); const domModule = require('../components/dom-renderer.js'); const debugModule = require('../components/debug-panel.js'); - const controlsModule = require('../components/document-controls.js'); + const controlsModule = require('../components/document-controls-legacy.js'); const { SectionManager } = sectionModule; const { DOMRenderer } = domModule; const { DebugPanel } = debugModule; - const { DocumentControls } = controlsModule; + const { DocumentControlsLegacy } = controlsModule; // Setup DOM container const container = document.createElement('div'); @@ -34,7 +34,7 @@ runner.describe('Real User Functionality Tests', () => { const sectionManager = new SectionManager(); const domRenderer = new DOMRenderer(sectionManager, container); const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); + const documentControls = new DocumentControlsLegacy(); // Setup document controls documentControls.create(); @@ -96,11 +96,11 @@ runner.describe('Real User Functionality Tests', () => { // Setup similar to above const sectionModule = require('../core/section-manager.js'); const domModule = require('../components/dom-renderer.js'); - const controlsModule = require('../components/document-controls.js'); + const controlsModule = require('../components/document-controls-legacy.js'); const { SectionManager } = sectionModule; const { DOMRenderer } = domModule; - const { DocumentControls } = controlsModule; + const { DocumentControlsLegacy } = controlsModule; const container = document.createElement('div'); container.innerHTML = '
'; @@ -108,7 +108,7 @@ runner.describe('Real User Functionality Tests', () => { const sectionManager = new SectionManager(); const domRenderer = new DOMRenderer(sectionManager, container); - const documentControls = new DocumentControls(); + const documentControls = new DocumentControlsLegacy(); documentControls.create(); @@ -195,12 +195,12 @@ runner.describe('Real User Functionality Tests', () => { const sectionModule = require('../core/section-manager.js'); const domModule = require('../components/dom-renderer.js'); const debugModule = require('../components/debug-panel.js'); - const controlsModule = require('../components/document-controls.js'); + const controlsModule = require('../components/document-controls-legacy.js'); const { SectionManager } = sectionModule; const { DOMRenderer } = domModule; const { DebugPanel } = debugModule; - const { DocumentControls } = controlsModule; + const { DocumentControlsLegacy } = controlsModule; const container = document.createElement('div'); container.innerHTML = '
'; @@ -209,7 +209,7 @@ runner.describe('Real User Functionality Tests', () => { const sectionManager = new SectionManager(); const domRenderer = new DOMRenderer(sectionManager, container); const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); + const documentControls = new DocumentControlsLegacy(); documentControls.create(); diff --git a/relicts/AllControlsRudimentary.html b/capabilities/testdrive-jsui/relicts/AllControlsRudimentary.html similarity index 100% rename from relicts/AllControlsRudimentary.html rename to capabilities/testdrive-jsui/relicts/AllControlsRudimentary.html diff --git a/relicts/ControlFooter.html b/capabilities/testdrive-jsui/relicts/ControlFooter.html similarity index 100% rename from relicts/ControlFooter.html rename to capabilities/testdrive-jsui/relicts/ControlFooter.html diff --git a/relicts/DebugControlContent.html b/capabilities/testdrive-jsui/relicts/DebugControlContent.html similarity index 100% rename from relicts/DebugControlContent.html rename to capabilities/testdrive-jsui/relicts/DebugControlContent.html diff --git a/relicts/StatusPsychadelic.html b/capabilities/testdrive-jsui/relicts/StatusPsychadelic.html similarity index 100% rename from relicts/StatusPsychadelic.html rename to capabilities/testdrive-jsui/relicts/StatusPsychadelic.html diff --git a/capabilities/testdrive-jsui/scripts/list_components.py b/capabilities/testdrive-jsui/scripts/list_components.py new file mode 100755 index 00000000..e6782e04 --- /dev/null +++ b/capabilities/testdrive-jsui/scripts/list_components.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +""" +TestDrive-JSUI Component Lister + +Lists all available UI components with descriptions and key information. +""" + +import re +import json +from pathlib import Path +from typing import Dict, List, Optional, Tuple + + +class ComponentInfo: + """Information about a UI component.""" + + def __init__(self, name: str, file_path: str, description: str, + component_type: str, dependencies: List[str] = None, + methods: List[str] = None, classes: List[str] = None): + self.name = name + self.file_path = file_path + self.description = description + self.component_type = component_type + self.dependencies = dependencies or [] + self.methods = methods or [] + self.classes = classes or [] + + +class ComponentAnalyzer: + """Analyzes JavaScript component files to extract information.""" + + def __init__(self, capability_root: Path): + self.capability_root = capability_root + self.js_root = capability_root / "js" + + def analyze_file(self, file_path: Path) -> Optional[ComponentInfo]: + """Analyze a single JavaScript file for component information.""" + if not file_path.exists(): + return None + + content = file_path.read_text() + relative_path = str(file_path.relative_to(self.capability_root)) + + # Extract component name from file path with special handling for acronyms and legacy naming + component_name = file_path.stem.replace('-', ' ').title().replace(' ', '') + + # Special handling for common acronyms and naming patterns + acronym_mappings = { + 'DomRenderer': 'DOMRenderer', + 'DomControls': 'DOMControls', + 'HtmlRenderer': 'HTMLRenderer', + 'CssProcessor': 'CSSProcessor', + 'JsEngine': 'JSEngine', + 'ApiClient': 'APIClient', + 'UrlHandler': 'URLHandler', + 'DocumentControlsLegacy': 'DocumentControlsLegacy' + } + + component_name = acronym_mappings.get(component_name, component_name) + + # Extract description from file header comment + description = self._extract_description(content) + + # Determine component type from path + component_type = self._determine_component_type(file_path) + + # Extract dependencies + dependencies = self._extract_dependencies(content) + + # Extract class names + classes = self._extract_classes(content) + + # Extract method names (public methods) + methods = self._extract_public_methods(content) + + return ComponentInfo( + name=component_name, + file_path=relative_path, + description=description, + component_type=component_type, + dependencies=dependencies, + methods=methods, + classes=classes + ) + + def _extract_description(self, content: str) -> str: + """Extract description from file header comments.""" + # Look for block comment at start of file + block_comment_match = re.search(r'/\*\*(.*?)\*/', content, re.DOTALL) + if block_comment_match: + comment_lines = block_comment_match.group(1).strip() + # Clean up comment formatting + lines = [] + for line in comment_lines.split('\n'): + line = line.strip().lstrip('*').strip() + if line and not line.startswith('Dependencies:') and not line.startswith('Extracted from'): + lines.append(line) + elif line.startswith('Dependencies:'): + break + + description = ' '.join(lines) + # Take first sentence or reasonable chunk + if '.' in description: + first_sentence = description.split('.')[0] + '.' + if len(first_sentence) < 200: + return first_sentence + + # Fallback to first 150 characters + return description[:150] + '...' if len(description) > 150 else description + + # Fallback to single-line comments + line_comment_match = re.search(r'^\s*//\s*(.+)', content, re.MULTILINE) + if line_comment_match: + return line_comment_match.group(1).strip() + + return "Component implementation" + + def _determine_component_type(self, file_path: Path) -> str: + """Determine component type from file path.""" + path_str = str(file_path) + + # Check for legacy components first + if 'legacy' in path_str.lower(): + if 'components' in path_str: + return "Legacy UI Component" + elif 'controls' in path_str: + return "Legacy Control" + else: + return "Legacy Module" + + # Standard component types + if 'core' in path_str: + return "Core" + elif 'components' in path_str: + return "UI Component" + elif 'controls' in path_str: + return "Control" + elif 'utils' in path_str: + return "Utility" + else: + return "Module" + + def _extract_dependencies(self, content: str) -> List[str]: + """Extract dependencies mentioned in comments or imports.""" + dependencies = [] + + # Look for Dependencies section in comments + dep_match = re.search(r'Dependencies:\s*\n(.*?)(?:\*/|\n\s*\n)', content, re.DOTALL) + if dep_match: + dep_text = dep_match.group(1) + # Extract dependency names from bullet points + for line in dep_text.split('\n'): + line = line.strip().lstrip('*-').strip() + if line and not line.startswith('None'): + # Clean up dependency description + dep_name = line.split('(')[0].split('-')[0].strip() + if dep_name: + dependencies.append(dep_name) + + # Look for import statements or requires + import_matches = re.findall(r'(?:import|require)\s*\(?[\'"]([^\'\"]+)[\'"]', content) + dependencies.extend(import_matches) + + return list(set(dependencies)) # Remove duplicates + + def _extract_classes(self, content: str) -> List[str]: + """Extract class names from the file.""" + class_matches = re.findall(r'class\s+([A-Z][a-zA-Z0-9_]*)', content) + return class_matches + + def _extract_public_methods(self, content: str) -> List[str]: + """Extract public method names from classes.""" + methods = [] + + # Look for method definitions (not starting with _) + method_matches = re.findall(r'^\s+([a-zA-Z][a-zA-Z0-9_]*)\s*\(.*?\)\s*{', content, re.MULTILINE) + + # Filter out private methods (starting with _) and constructors + public_methods = [m for m in method_matches if not m.startswith('_') and m != 'constructor'] + + return list(set(public_methods)) # Remove duplicates + + +def list_components(output_format: str = "table") -> None: + """List all UI components in the capability.""" + + capability_root = Path(__file__).parent.parent + analyzer = ComponentAnalyzer(capability_root) + + # Find all JavaScript component files + component_files = [] + js_root = capability_root / "js" + + if js_root.exists(): + # Get files from components, core, and other directories + for pattern in ["**/*.js"]: + for file_path in js_root.glob(pattern): + # Skip test files and node_modules + path_str = str(file_path) + if ('test' not in file_path.name.lower() and + '/tests/' not in path_str and + 'node_modules' not in path_str and + not file_path.name.lower().endswith('.test.js')): + component_files.append(file_path) + + # Analyze each file + components = [] + for file_path in sorted(component_files): + component_info = analyzer.analyze_file(file_path) + if component_info: + components.append(component_info) + + if not components: + print("❌ No UI components found in the capability") + return + + # Output in requested format + if output_format == "json": + _output_json(components) + elif output_format == "detailed": + _output_detailed(components) + else: + _output_table(components) + + +def _output_table(components: List[ComponentInfo]) -> None: + """Output components in table format.""" + print("πŸ§ͺ TestDrive-JSUI UI Components") + print("=" * 60) + print() + + # Group by type + by_type = {} + for comp in components: + if comp.component_type not in by_type: + by_type[comp.component_type] = [] + by_type[comp.component_type].append(comp) + + for comp_type, comps in sorted(by_type.items()): + print(f"πŸ“¦ {comp_type} Components ({len(comps)})") + print("-" * 40) + + for comp in sorted(comps, key=lambda x: x.name): + print(f" πŸ”§ {comp.name}") + print(f" πŸ“„ {comp.file_path}") + print(f" πŸ“ {comp.description}") + if comp.classes: + print(f" πŸ—οΈ Classes: {', '.join(comp.classes)}") + if comp.methods: + key_methods = comp.methods[:4] # Show first 4 methods + methods_str = ', '.join(key_methods) + if len(comp.methods) > 4: + methods_str += f" (+{len(comp.methods) - 4} more)" + print(f" βš™οΈ Methods: {methods_str}") + print() + + print() + + +def _output_detailed(components: List[ComponentInfo]) -> None: + """Output components with detailed information.""" + print("πŸ§ͺ TestDrive-JSUI UI Components - Detailed View") + print("=" * 60) + print() + + for i, comp in enumerate(sorted(components, key=lambda x: x.name), 1): + print(f"{i}. {comp.name}") + print(f" Type: {comp.component_type}") + print(f" File: {comp.file_path}") + print(f" Description: {comp.description}") + + if comp.classes: + print(f" Classes: {', '.join(comp.classes)}") + + if comp.methods: + print(f" Public Methods ({len(comp.methods)}): {', '.join(sorted(comp.methods))}") + + if comp.dependencies: + print(f" Dependencies: {', '.join(comp.dependencies)}") + + print() + + +def _output_json(components: List[ComponentInfo]) -> None: + """Output components in JSON format.""" + data = { + "capability": "testdrive-jsui", + "total_components": len(components), + "components": [] + } + + for comp in sorted(components, key=lambda x: x.name): + data["components"].append({ + "name": comp.name, + "type": comp.component_type, + "file_path": comp.file_path, + "description": comp.description, + "classes": comp.classes, + "methods": comp.methods, + "dependencies": comp.dependencies + }) + + print(json.dumps(data, indent=2)) + + +if __name__ == "__main__": + import sys + + output_format = "table" + if len(sys.argv) > 1: + format_arg = sys.argv[1].lower() + if format_arg in ["json", "detailed", "table"]: + output_format = format_arg + else: + print(f"❌ Invalid format: {format_arg}") + print("Usage: python list_components.py [table|detailed|json]") + sys.exit(1) + + list_components(output_format) \ No newline at end of file diff --git a/capabilities/testdrive-jsui/test-control-base.html b/capabilities/testdrive-jsui/test-control-base.html new file mode 100644 index 00000000..6ae95633 --- /dev/null +++ b/capabilities/testdrive-jsui/test-control-base.html @@ -0,0 +1,111 @@ + + + + + + Control Base Test + + + +
+

Control Base Functionality Test

+

This page tests the improved ControlBase functionality. You should see:

+
    +
  1. Icon-only collapsed state: Controls start as small icons
  2. +
  3. Click to expand: Click the icon to expand the control
  4. +
  5. Drag functionality: When expanded, drag by the header to move
  6. +
  7. Bottom-left resize: When expanded, grab the ↙ corner to resize
  8. +
  9. Close button (βœ•): Returns control to original position
  10. +
  11. Header toggle: Click the title to show/hide content
  12. +
+
+ + + + + + + \ No newline at end of file diff --git a/capabilities/testdrive-jsui/tests/test_complete.html b/capabilities/testdrive-jsui/tests/test_complete.html new file mode 100644 index 00000000..a7431280 --- /dev/null +++ b/capabilities/testdrive-jsui/tests/test_complete.html @@ -0,0 +1,191 @@ + + + + + + + Complete UI Test + + + + + + + + + + + +
+

Complete UI Test

+

This document tests the complete UI control system with all controls.

+

Content Section

+

This section has various content types to test the controls:

+

Lists

+
    +
  • Item 1
  • +
  • Item 2
  • +
  • Item 3
  • +
+

Code Example

+
console.log('Hello World');
+
+ +

Table

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureStatus
Status Controlβœ“ Working
Debug Controlβœ“ Working
Contents Controlβœ“ Working
Edit Controlβœ“ Working
+

Final Section

+

More content to test the table of contents functionality.

+
+

-- html from markdown by MarkiTect on 2025-11-11 23:46:11 by worsch

+
+ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/capabilities/testdrive-jsui/tests/test_component_listing.py b/capabilities/testdrive-jsui/tests/test_component_listing.py new file mode 100644 index 00000000..a0fe2758 --- /dev/null +++ b/capabilities/testdrive-jsui/tests/test_component_listing.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +""" +Test for Component Listing Functionality + +Tests that all expected UI components are properly discovered and listed +by the component analysis system. +""" + +import sys +from pathlib import Path +import json +import subprocess +import pytest + +# Add the scripts directory to path +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) + +from list_components import ComponentAnalyzer, list_components + + +@pytest.mark.javascript +class TestComponentListing: + """Test cases for component listing functionality.""" + + def setup_method(self): + """Setup test environment.""" + self.capability_root = Path(__file__).parent.parent + self.analyzer = ComponentAnalyzer(self.capability_root) + + def test_expected_panel_components_exist(self): + """Test that all expected panel components are found.""" + expected_panels = { + 'ContentsControl': 'js/controls/contents-control.js', + 'StatusControl': 'js/controls/status-control.js', + 'EditControl': 'js/controls/edit-control.js', + 'DebugControl': 'js/controls/debug-control.js', + 'DebugPanel': 'js/components/debug-panel.js', + 'DocumentControlsLegacy': 'js/components/document-controls-legacy.js', + 'SectionManager': 'js/core/section-manager.js', + 'DOMRenderer': 'js/components/dom-renderer.js' + } + + # Get actual components found by analyzer + js_files = [] + js_root = self.capability_root / "js" + + if js_root.exists(): + for pattern in ["**/*.js"]: + for file_path in js_root.glob(pattern): + path_str = str(file_path) + if ('test' not in file_path.name.lower() and + '/tests/' not in path_str and + 'node_modules' not in path_str and + not file_path.name.lower().endswith('.test.js')): + js_files.append(file_path) + + # Analyze each file + found_components = {} + for file_path in sorted(js_files): + component_info = self.analyzer.analyze_file(file_path) + if component_info: + found_components[component_info.name] = component_info.file_path + + # Check that all expected panels are found + missing_components = [] + for expected_name, expected_path in expected_panels.items(): + if expected_name not in found_components: + missing_components.append(f"{expected_name} (expected at {expected_path})") + elif found_components[expected_name] != expected_path: + missing_components.append( + f"{expected_name} found at {found_components[expected_name]} " + f"but expected at {expected_path}" + ) + + if missing_components: + print(f"\n❌ Missing or misplaced components:") + for missing in missing_components: + print(f" - {missing}") + print(f"\nβœ… Found components:") + for name, path in found_components.items(): + print(f" - {name}: {path}") + + assert False, f"Missing {len(missing_components)} expected components: {missing_components}" + + print(f"βœ… All {len(expected_panels)} expected components found!") + return True + + def test_component_lister_json_output_completeness(self): + """Test that JSON output includes all expected components with proper structure.""" + # Capture JSON output + result = subprocess.run([ + sys.executable, + str(self.capability_root / "scripts" / "list_components.py"), + "json" + ], capture_output=True, text=True, cwd=str(self.capability_root)) + + assert result.returncode == 0, f"Component lister failed: {result.stderr}" + + try: + data = json.loads(result.stdout) + except json.JSONDecodeError as e: + assert False, f"Invalid JSON output: {e}" + + # Verify JSON structure + assert "capability" in data + assert "total_components" in data + assert "components" in data + assert data["capability"] == "testdrive-jsui" + + # Verify each component has required fields + required_fields = ["name", "type", "file_path", "description", "classes", "methods"] + for component in data["components"]: + for field in required_fields: + assert field in component, f"Component {component.get('name')} missing field: {field}" + + # Check for expected panel types + component_names = {comp["name"] for comp in data["components"]} + expected_controls = {"ContentsControl", "StatusControl", "EditControl", "DebugControl"} + + found_controls = component_names.intersection(expected_controls) + missing_controls = expected_controls - found_controls + + if missing_controls: + print(f"\n❌ Missing control components: {missing_controls}") + print(f"βœ… Found components: {component_names}") + assert False, f"Missing control components: {missing_controls}" + + return True + + def test_component_descriptions_are_meaningful(self): + """Test that all components have meaningful descriptions.""" + result = subprocess.run([ + sys.executable, + str(self.capability_root / "scripts" / "list_components.py"), + "json" + ], capture_output=True, text=True, cwd=str(self.capability_root)) + + assert result.returncode == 0, f"Component lister failed: {result.stderr}" + data = json.loads(result.stdout) + + generic_descriptions = ["Component implementation", ""] + components_with_poor_descriptions = [] + + for component in data["components"]: + description = component["description"].strip() + if (not description or + description in generic_descriptions or + len(description) < 20): + components_with_poor_descriptions.append(component["name"]) + + if components_with_poor_descriptions: + print(f"\n❌ Components with poor descriptions: {components_with_poor_descriptions}") + for comp in data["components"]: + if comp["name"] in components_with_poor_descriptions: + print(f" - {comp['name']}: '{comp['description']}'") + + assert False, f"Components need better descriptions: {components_with_poor_descriptions}" + + return True + + +def run_tests(): + """Run all component listing tests.""" + test_instance = TestComponentListing() + test_methods = [method for method in dir(test_instance) if method.startswith('test_')] + + results = {} + for method_name in test_methods: + print(f"\nπŸ§ͺ Running {method_name}") + print("=" * 60) + + try: + test_instance.setup_method() + method = getattr(test_instance, method_name) + result = method() + results[method_name] = True + print(f"βœ… {method_name} PASSED") + + except Exception as e: + results[method_name] = False + print(f"❌ {method_name} FAILED: {e}") + import traceback + traceback.print_exc() + + # Summary + passed = sum(1 for result in results.values() if result) + total = len(results) + + print(f"\nπŸ“Š Test Summary:") + print(f" Passed: {passed}/{total}") + print(f" Failed: {total - passed}/{total}") + + if passed == total: + print("βœ… All tests passed!") + return True + else: + print("❌ Some tests failed!") + return False + + +if __name__ == "__main__": + success = run_tests() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/capabilities/testdrive-jsui/tests/test_guardrail_js.html b/capabilities/testdrive-jsui/tests/test_guardrail_js.html new file mode 100644 index 00000000..357851d9 --- /dev/null +++ b/capabilities/testdrive-jsui/tests/test_guardrail_js.html @@ -0,0 +1,145 @@ + + + + + + + Guardrail Principle Test - JavaScript Controls + + + +
+

Guardrail Principle Test Page

+ +
+

Test Section 1

+

This is a test paragraph to verify that the status control can properly count and analyze document content.

+

Another paragraph with some formatted text and emphasis.

+
+ +
+

Test Subsection with Table

+ + + + + + + + + + + + + + + + +
Column 1Column 2Column 3
Row 1, Cell 1Row 1, Cell 2Row 1, Cell 3
Row 2, Cell 1Row 2, Cell 2Row 2, Cell 3
+
+ +
+

Test with Images

+

Testing image counting (placeholder images):

+ Placeholder 1 + Placeholder 2 +
+ +
+

Test with Lists

+
    +
  • List item 1
  • +
  • List item 2 with inline code
  • +
  • List item 3
  • +
+ +
    +
  1. Ordered item 1
  2. +
  3. Ordered item 2
  4. +
+
+ +
+ This is a blockquote to test various content types that the status control should analyze. +
+ +

+// This is a code block
+function testFunction() {
+    return "Testing code block counting";
+}
+        
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/capabilities/testdrive-jsui/tests/test_integration.html b/capabilities/testdrive-jsui/tests/test_integration.html new file mode 100644 index 00000000..8603a47a --- /dev/null +++ b/capabilities/testdrive-jsui/tests/test_integration.html @@ -0,0 +1,213 @@ + + + + + + + Integration Test Document + + + + + + + + + + + +
+

Integration Test Document

+

This document tests the JavaScript controls integration with the HTML output after the Guardrail Principle refactoring.

+

Recent Changes

+

Latest Commit (dbde13e)

+
    +
  • Enhanced control system with improved UI and debug functionality
  • +
  • Added resize functionality to all controls with hover-only visibility
  • +
  • Implemented small circle resize handles positioned in lower-right corner
  • +
  • Added header-only toggle mode for space-efficient control management
  • +
  • Created independent IndexedDB-based debug system with selection filtering
  • +
+

Previous Commit (3839a67)

+
    +
  • Fixed control positioning and drag behavior
  • +
  • Updated compass positioning to be top-aligned instead of center-aligned
  • +
  • Fixed drag offset calculation to maintain cursor position at icon
  • +
  • Ensured expanded controls appear top-aligned with anchor position
  • +
+

Test Content

+

Headers

+

This document contains various content types to test the status control functionality.

+

Subsection

+

Content in subsections should be properly counted.

+

Lists

+
    +
  • Item 1: Testing list counting
  • +
  • Item 2: Multiple items
  • +
  • Item 3: Final item
  • +
+

Tables

+ + + + + + + + + + + + + + + + + + + + +
Column AColumn BColumn C
Row 1ARow 1BRow 1C
Row 2ARow 2BRow 2C
+

Code Block

+
def test_function():
+    return "This code block should be counted"
+
+ +

Blockquote

+
+

This is a blockquote that should be analyzed by the status control.

+
+

Expected Behavior

+

The JavaScript controls should: +1. Initialize successfully with proper error handling +2. Display accurate document statistics +3. Provide interactive drag/resize functionality +4. Work with the debug system integration +5. Handle errors gracefully per the Guardrail Principle

+

This test will verify that our external JavaScript files work correctly with the HTML template system.

+
+

-- html from markdown by MarkiTect on 2025-11-11 22:10:30 by worsch

+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/capabilities/testdrive-jsui/tests/test_js_fixes.py b/capabilities/testdrive-jsui/tests/test_js_fixes.py new file mode 100644 index 00000000..e0d1ff30 --- /dev/null +++ b/capabilities/testdrive-jsui/tests/test_js_fixes.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +""" +Test JavaScript fixes for const redeclaration and MarkitectMain issues +""" + +import sys +from pathlib import Path +import re + +# Add project root to path for imports +project_root = Path(__file__).parent.parent.parent +sys.path.insert(0, str(project_root)) + +def test_javascript_fixes(): + """Test that JavaScript const redeclaration and MarkitectMain issues are resolved.""" + + print("πŸ”§ Testing JavaScript Fixes") + print("=" * 50) + + try: + # Test 1: Check for const declarations in loaded files + print("1️⃣ Checking for const declaration conflicts...") + + from markitect.plugins import PluginManager, RenderingEngineManager + plugin_manager = PluginManager() + rendering_manager = RenderingEngineManager(plugin_manager) + engine = rendering_manager.get_engine('testdrive-jsui') + + required_assets = engine.get_required_assets() + js_files = required_assets.get('js', []) + + print(f" πŸ“„ JavaScript files to be loaded: {len(js_files)}") + + const_declarations = {} + capability_root = Path(__file__).parent.parent + for js_file in js_files: + # Check if file exists in capability directory first + file_path = capability_root / js_file + if file_path.exists(): + content = file_path.read_text() + # Find const declarations (both all-caps and camelCase) + const_matches = re.findall(r'^const\s+([A-Za-z_][A-Za-z0-9_]*)\s*=', content, re.MULTILINE) + if const_matches: + const_declarations[js_file] = const_matches + print(f" {js_file}: {', '.join(const_matches)}") + else: + print(f" {js_file}: File not found in capability directory") + + # Check for duplicates + all_consts = [] + for file, consts in const_declarations.items(): + all_consts.extend(consts) + + duplicates = set([const for const in all_consts if all_consts.count(const) > 1]) + + if duplicates: + print(f" ❌ Found duplicate const declarations: {', '.join(duplicates)}") + return False + else: + print(f" βœ… No duplicate const declarations found") + + # Test 2: Verify key components are in the loaded files + print(f"\n2️⃣ Checking key component availability...") + + # Look for important components instead of MarkitectMain + key_components = ['EditState', 'SectionType', 'DocumentControls', 'SectionManager', 'DOMRenderer'] + found_components = {} + + for file, consts in const_declarations.items(): + for component in key_components: + if component in consts: + if component in found_components: + found_components[component].append(file) + else: + found_components[component] = [file] + + missing_components = [comp for comp in key_components if comp not in found_components] + if missing_components: + print(f" ⚠️ Some components not found: {', '.join(missing_components)}") + + duplicate_components = {comp: files for comp, files in found_components.items() if len(files) > 1} + if duplicate_components: + print(f" ❌ Duplicate components found: {duplicate_components}") + return False + + if found_components: + print(f" βœ… Found key components: {', '.join(found_components.keys())}") + else: + print(f" ℹ️ No key components found (might use different patterns)") + + # Test 3: Verify file structure and loading order + print(f"\n3️⃣ Checking file structure and loading order...") + + main_files = [f for f in js_files if 'main' in f.lower()] + if main_files: + print(f" βœ… Main files found: {', '.join(main_files)}") + else: + print(f" ℹ️ No explicit main files found in asset list") + + # Check for core components in the expected order + core_files = [f for f in js_files if 'core' in f or 'components' in f or 'controls' in f] + if core_files: + print(f" βœ… Core component files found: {len(core_files)} files") + else: + print(f" ⚠️ No core component files found") + + # Test 4: Generate and verify HTML output + print(f"\n4️⃣ Testing HTML generation...") + + from markitect.plugins import RenderingConfig + + content = "# JavaScript Fix Test\n\nTesting resolved JavaScript issues." + output_dir = Path('/tmp/test_js_fixes_verification') + output_dir.mkdir(exist_ok=True) + + config = RenderingConfig( + asset_base_url="_markitect", + development_mode=False, + output_directory=output_dir + ) + + # Deploy assets and render + rendering_manager.deploy_engine_assets('testdrive-jsui', config) + html_content = engine.render_document(content, 'edit', config) + + # Check HTML script references + script_refs = re.findall(r'