Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
242 lines
6.9 KiB
JavaScript
242 lines
6.9 KiB
JavaScript
/**
|
|
* ds-base-tool.js
|
|
* Base class for all DSS tool components
|
|
*
|
|
* Enforces DSS coding standards:
|
|
* - Shadow DOM encapsulation
|
|
* - Automatic event listener cleanup via AbortController
|
|
* - Constructable Stylesheets support
|
|
* - Standardized lifecycle methods
|
|
* - Logger utility integration
|
|
*
|
|
* Reference: .knowledge/dss-coding-standards.json
|
|
*/
|
|
|
|
import { logger } from '../../utils/logger.js';
|
|
|
|
/**
|
|
* Base class for DSS tool components
|
|
* All tool components should extend this class to ensure compliance with DSS standards
|
|
*/
|
|
export default class DSBaseTool extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
|
|
// WC-001: Shadow DOM Required
|
|
this.attachShadow({ mode: 'open' });
|
|
|
|
// EVENT-003: Use AbortController for cleanup
|
|
this._abortController = new AbortController();
|
|
|
|
// Track component state
|
|
this._isConnected = false;
|
|
|
|
logger.debug(`[${this.constructor.name}] Constructor initialized`);
|
|
}
|
|
|
|
/**
|
|
* Standard Web Component lifecycle: called when element is added to DOM
|
|
*/
|
|
connectedCallback() {
|
|
this._isConnected = true;
|
|
logger.debug(`[${this.constructor.name}] Connected to DOM`);
|
|
|
|
// Render the component
|
|
this.render();
|
|
|
|
// Setup event listeners after render
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
/**
|
|
* Standard Web Component lifecycle: called when element is removed from DOM
|
|
* Automatically cleans up all event listeners via AbortController
|
|
*/
|
|
disconnectedCallback() {
|
|
this._isConnected = false;
|
|
|
|
// EVENT-003: Abort all event listeners
|
|
this._abortController.abort();
|
|
|
|
// Create new controller for potential re-connection
|
|
this._abortController = new AbortController();
|
|
|
|
logger.debug(`[${this.constructor.name}] Disconnected from DOM, listeners cleaned up`);
|
|
}
|
|
|
|
/**
|
|
* Centralized event binding with automatic cleanup
|
|
* @param {EventTarget} target - Element to attach listener to
|
|
* @param {string} type - Event type (e.g., 'click', 'mouseover')
|
|
* @param {Function} handler - Event handler function
|
|
* @param {Object} options - Additional addEventListener options
|
|
*/
|
|
bindEvent(target, type, handler, options = {}) {
|
|
if (!target || typeof handler !== 'function') {
|
|
logger.warn(`[${this.constructor.name}] Invalid event binding attempt`, { target, type, handler });
|
|
return;
|
|
}
|
|
|
|
// Add AbortController signal to options
|
|
const eventOptions = {
|
|
...options,
|
|
signal: this._abortController.signal
|
|
};
|
|
|
|
target.addEventListener(type, handler, eventOptions);
|
|
|
|
logger.debug(`[${this.constructor.name}] Event bound: ${type} on`, target);
|
|
}
|
|
|
|
/**
|
|
* Event delegation helper for handling multiple elements with data-action attributes
|
|
* @param {string} selector - CSS selector for the container element
|
|
* @param {string} eventType - Event type to listen for
|
|
* @param {Function} handler - Handler function that receives (action, event)
|
|
*/
|
|
delegateEvents(selector, eventType, handler) {
|
|
const container = this.shadowRoot.querySelector(selector);
|
|
if (!container) {
|
|
logger.warn(`[${this.constructor.name}] Event delegation container not found: ${selector}`);
|
|
return;
|
|
}
|
|
|
|
this.bindEvent(container, eventType, (e) => {
|
|
// Find element with data-action attribute
|
|
const target = e.target.closest('[data-action]');
|
|
if (target) {
|
|
const action = target.dataset.action;
|
|
handler(action, e, target);
|
|
}
|
|
});
|
|
|
|
logger.debug(`[${this.constructor.name}] Event delegation setup for ${eventType} on ${selector}`);
|
|
}
|
|
|
|
/**
|
|
* Inject CSS styles using Constructable Stylesheets
|
|
* STYLE-002: Use Constructable Stylesheets for shared styles
|
|
* @param {string} cssString - CSS string to inject
|
|
*/
|
|
adoptStyles(cssString) {
|
|
try {
|
|
const sheet = new CSSStyleSheet();
|
|
sheet.replaceSync(cssString);
|
|
|
|
// Append to existing stylesheets
|
|
this.shadowRoot.adoptedStyleSheets = [
|
|
...this.shadowRoot.adoptedStyleSheets,
|
|
sheet
|
|
];
|
|
|
|
logger.debug(`[${this.constructor.name}] Styles adopted (${cssString.length} bytes)`);
|
|
} catch (error) {
|
|
logger.error(`[${this.constructor.name}] Failed to adopt styles:`, error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set multiple attributes at once
|
|
* @param {Object} attrs - Object with attribute key-value pairs
|
|
*/
|
|
setAttributes(attrs) {
|
|
Object.entries(attrs).forEach(([key, value]) => {
|
|
if (value !== null && value !== undefined) {
|
|
this.setAttribute(key, value);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get attribute with fallback value
|
|
* @param {string} name - Attribute name
|
|
* @param {*} defaultValue - Default value if attribute doesn't exist
|
|
* @returns {string|*} Attribute value or default
|
|
*/
|
|
getAttr(name, defaultValue = null) {
|
|
return this.hasAttribute(name) ? this.getAttribute(name) : defaultValue;
|
|
}
|
|
|
|
/**
|
|
* Render method - MUST be implemented by subclasses
|
|
* Should set shadowRoot.innerHTML with component template
|
|
*/
|
|
render() {
|
|
throw new Error(`${this.constructor.name} must implement render() method`);
|
|
}
|
|
|
|
/**
|
|
* Setup event listeners - should be implemented by subclasses
|
|
* Use this.bindEvent() or this.delegateEvents() for automatic cleanup
|
|
*/
|
|
setupEventListeners() {
|
|
// Override in subclass if needed
|
|
logger.debug(`[${this.constructor.name}] setupEventListeners() not implemented (optional)`);
|
|
}
|
|
|
|
/**
|
|
* Trigger re-render (useful for state changes)
|
|
*/
|
|
rerender() {
|
|
if (this._isConnected) {
|
|
// Abort existing listeners before re-render
|
|
this._abortController.abort();
|
|
this._abortController = new AbortController();
|
|
|
|
// Re-render and re-setup listeners
|
|
this.render();
|
|
this.setupEventListeners();
|
|
|
|
logger.debug(`[${this.constructor.name}] Component re-rendered`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper: Query single element in shadow DOM
|
|
* @param {string} selector - CSS selector
|
|
* @returns {Element|null}
|
|
*/
|
|
$(selector) {
|
|
return this.shadowRoot.querySelector(selector);
|
|
}
|
|
|
|
/**
|
|
* Helper: Query multiple elements in shadow DOM
|
|
* @param {string} selector - CSS selector
|
|
* @returns {NodeList}
|
|
*/
|
|
$$(selector) {
|
|
return this.shadowRoot.querySelectorAll(selector);
|
|
}
|
|
|
|
/**
|
|
* Helper: Escape HTML to prevent XSS
|
|
* SECURITY-001: Sanitize user input
|
|
* @param {string} str - String to escape
|
|
* @returns {string} Escaped string
|
|
*/
|
|
escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
/**
|
|
* Helper: Dispatch custom event
|
|
* @param {string} eventName - Event name
|
|
* @param {*} detail - Event detail payload
|
|
* @param {Object} options - Event options
|
|
*/
|
|
emit(eventName, detail = null, options = {}) {
|
|
const event = new CustomEvent(eventName, {
|
|
detail,
|
|
bubbles: true,
|
|
composed: true, // Cross shadow DOM boundary
|
|
...options
|
|
});
|
|
|
|
this.dispatchEvent(event);
|
|
logger.debug(`[${this.constructor.name}] Event emitted: ${eventName}`, detail);
|
|
}
|
|
}
|