Initial commit: Clean DSS implementation
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
This commit is contained in:
241
admin-ui/js/components/base/ds-base-tool.js
Normal file
241
admin-ui/js/components/base/ds-base-tool.js
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user