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:
Digital Production Factory
2025-12-09 18:45:48 -03:00
commit 276ed71f31
884 changed files with 373737 additions and 0 deletions

View File

@@ -0,0 +1,417 @@
/**
* DsComponentBase - Base class for all design system components
*
* Provides standardized:
* - Component lifecycle (connectedCallback, disconnectedCallback, attributeChangedCallback)
* - Standard attributes (variant, size, disabled, loading, aria-* attributes)
* - Standard methods (focus(), blur())
* - Theme change handling
* - Accessibility features (WCAG 2.1 AA)
* - Event emission patterns (ds-* namespaced events)
*
* All Web Components should extend this class to ensure API consistency.
*
* Usage:
* class DsButton extends DsComponentBase {
* static get observedAttributes() {
* return [...super.observedAttributes(), 'type'];
* }
* }
*/
import StylesheetManager from '../core/stylesheet-manager.js';
export class DsComponentBase extends HTMLElement {
/**
* Standard observed attributes all components should support
* Subclasses should extend this list with component-specific attributes
*/
static get observedAttributes() {
return [
// State management
'disabled',
'loading',
// Accessibility
'aria-label',
'aria-disabled',
'aria-expanded',
'aria-hidden',
'aria-pressed',
'aria-selected',
'aria-invalid',
'aria-describedby',
'aria-labelledby',
// Focus management
'tabindex'
];
}
/**
* Initialize component
* Subclasses should call super.constructor()
*/
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Initialize standard properties
this._disabled = false;
this._loading = false;
this._initialized = false;
this._cleanup = [];
this._themeObserver = null;
this._resizeObserver = null;
}
/**
* Called when component is inserted into DOM
* Loads stylesheets, syncs attributes, and renders
*/
async connectedCallback() {
try {
// Attach stylesheets
await StylesheetManager.attachStyles(this.shadowRoot);
// Sync HTML attributes to JavaScript properties
this._syncAttributesToProperties();
// Initialize theme observer for dark/light mode changes
this._initializeThemeObserver();
// Allow subclass to setup event listeners
this.setupEventListeners?.();
// Initial render
this._initialized = true;
this.render?.();
// Emit connected event for testing/debugging
this.emit('ds-component-connected', {
component: this.constructor.name
});
} catch (error) {
console.error(`[${this.constructor.name}] Error in connectedCallback:`, error);
this.emit('ds-component-error', { error: error.message });
}
}
/**
* Called when component is removed from DOM
* Cleanup event listeners and observers
*/
disconnectedCallback() {
// Allow subclass to cleanup
this.cleanupEventListeners?.();
// Remove theme observer
if (this._themeObserver) {
window.removeEventListener('theme-changed', this._themeObserver);
this._themeObserver = null;
}
// Disconnect resize observer if present
if (this._resizeObserver) {
this._resizeObserver.disconnect();
this._resizeObserver = null;
}
// Cleanup all tracked listeners
this._cleanup.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});
this._cleanup = [];
}
/**
* Called when observed attributes change
* Subclasses can override but should call super.attributeChangedCallback()
*/
attributeChangedCallback(name, oldValue, newValue) {
if (!this._initialized || oldValue === newValue) return;
// Handle standard attributes
switch (name) {
case 'disabled':
this._disabled = newValue !== null;
this._updateAccessibility();
break;
case 'loading':
this._loading = newValue !== null;
break;
case 'aria-label':
case 'aria-disabled':
case 'aria-expanded':
case 'aria-hidden':
case 'aria-pressed':
case 'aria-selected':
case 'aria-invalid':
this._updateAccessibility();
break;
case 'tabindex':
// Update tabindex if changed
this.setAttribute('tabindex', newValue || '0');
break;
}
// Re-render component
this.render?.();
}
/**
* Sync HTML attributes to JavaScript properties
* @private
*/
_syncAttributesToProperties() {
this._disabled = this.hasAttribute('disabled');
this._loading = this.hasAttribute('loading');
// Ensure accessible tabindex
if (!this.hasAttribute('tabindex')) {
this.setAttribute('tabindex', this._disabled ? '-1' : '0');
} else if (this._disabled && this.getAttribute('tabindex') !== '-1') {
this.setAttribute('tabindex', '-1');
}
}
/**
* Initialize theme observer to listen for dark/light mode changes
* @private
*/
_initializeThemeObserver() {
this._themeObserver = () => {
// Re-render when theme changes
this.render?.();
};
window.addEventListener('theme-changed', this._themeObserver);
}
/**
* Update accessibility attributes based on component state
* @private
*/
_updateAccessibility() {
// Update aria-disabled to match disabled state
this.setAttribute('aria-disabled', this._disabled);
// Ensure proper tab order when disabled
if (this._disabled) {
this.setAttribute('tabindex', '-1');
} else if (this.getAttribute('tabindex') === '-1') {
this.setAttribute('tabindex', '0');
}
}
/**
* Standard properties with getters/setters
*/
get disabled() { return this._disabled; }
set disabled(value) {
this._disabled = !!value;
value ? this.setAttribute('disabled', '') : this.removeAttribute('disabled');
}
get loading() { return this._loading; }
set loading(value) {
this._loading = !!value;
value ? this.setAttribute('loading', '') : this.removeAttribute('loading');
}
get ariaLabel() { return this.getAttribute('aria-label'); }
set ariaLabel(value) {
value ? this.setAttribute('aria-label', value) : this.removeAttribute('aria-label');
}
get ariaDescribedBy() { return this.getAttribute('aria-describedby'); }
set ariaDescribedBy(value) {
value ? this.setAttribute('aria-describedby', value) : this.removeAttribute('aria-describedby');
}
/**
* Standard methods for focus management
*/
focus(options) {
// Find first focusable element in shadow DOM
const focusable = this.shadowRoot.querySelector('button, input, [tabindex]');
if (focusable) {
focusable.focus(options);
}
}
blur() {
const focused = this.shadowRoot.activeElement;
if (focused && typeof focused.blur === 'function') {
focused.blur();
}
}
/**
* Emit custom event (ds-* namespaced)
* @param {string} eventName - Event name (without 'ds-' prefix)
* @param {object} detail - Event detail object
* @returns {boolean} Whether event was not prevented
*/
emit(eventName, detail = {}) {
const event = new CustomEvent(`ds-${eventName}`, {
detail,
composed: true, // Bubble out of shadow DOM
bubbles: true, // Standard bubbling
cancelable: true // Allow preventDefault()
});
return this.dispatchEvent(event);
}
/**
* Add event listener with automatic cleanup
* Listener is automatically removed in disconnectedCallback()
* @param {HTMLElement} element - Element to listen on
* @param {string} event - Event name
* @param {Function} handler - Event handler
* @param {object} [options] - Event listener options
*/
addEventListener(element, event, handler, options = false) {
element.addEventListener(event, handler, options);
this._cleanup.push({ element, event, handler });
}
/**
* Render method stub - override in subclass
* Called on initialization and on attribute changes
*/
render() {
// Override in subclass
}
/**
* Setup event listeners - override in subclass
* Called in connectedCallback after render
*/
setupEventListeners() {
// Override in subclass
}
/**
* Cleanup event listeners - override in subclass
* Called in disconnectedCallback
*/
cleanupEventListeners() {
// Override in subclass
}
/**
* Get computed CSS variable value
* @param {string} varName - CSS variable name (with or without --)
* @returns {string} CSS variable value
*/
getCSSVariable(varName) {
const name = varName.startsWith('--') ? varName : `--${varName}`;
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
/**
* Check if component is in dark mode
* @returns {boolean}
*/
isDarkMode() {
return document.documentElement.classList.contains('dark') ||
window.matchMedia('(prefers-color-scheme: dark)').matches;
}
/**
* Debounce function execution
* @param {Function} fn - Function to debounce
* @param {number} delay - Delay in milliseconds
* @returns {Function} Debounced function
*/
debounce(fn, delay = 300) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
/**
* Throttle function execution
* @param {Function} fn - Function to throttle
* @param {number} limit - Time limit in milliseconds
* @returns {Function} Throttled function
*/
throttle(fn, limit = 300) {
let inThrottle;
return (...args) => {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
/**
* Wait for an event
* @param {string} eventName - Event name to wait for
* @param {number} [timeout] - Optional timeout in milliseconds
* @returns {Promise} Resolves with event detail
*/
waitForEvent(eventName, timeout = null) {
return new Promise((resolve, reject) => {
const handler = (e) => {
this.removeEventListener(eventName, handler);
clearTimeout(timeoutId);
resolve(e.detail);
};
this.addEventListener(eventName, handler);
let timeoutId;
if (timeout) {
timeoutId = setTimeout(() => {
this.removeEventListener(eventName, handler);
reject(new Error(`Event '${eventName}' did not fire within ${timeout}ms`));
}, timeout);
}
});
}
/**
* Get HTML structure for rendering in shadow DOM
* Useful for preventing repeated string concatenation
* @param {string} html - HTML template
* @param {object} [data] - Data for template interpolation
* @returns {string} Rendered HTML
*/
renderTemplate(html, data = {}) {
return html.replace(/\{\{(\w+)\}\}/g, (match, key) => data[key] ?? match);
}
/**
* Static helper to create component with attributes
* @param {object} attrs - Attributes to set
* @returns {HTMLElement} Component instance
*/
static create(attrs = {}) {
const element = document.createElement(this.name.replace(/([A-Z])/g, '-$1').toLowerCase());
Object.entries(attrs).forEach(([key, value]) => {
if (value === true) {
element.setAttribute(key, '');
} else if (value !== false && value !== null && value !== undefined) {
element.setAttribute(key, value);
}
});
return element;
}
}
// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = { DsComponentBase };
}
// Make available globally
if (typeof window !== 'undefined') {
window.DsComponentBase = DsComponentBase;
}