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:
268
admin-ui/js/core/stylesheet-manager.js
Normal file
268
admin-ui/js/core/stylesheet-manager.js
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* StylesheetManager - Manages shared stylesheets for Web Components
|
||||
*
|
||||
* Implements constructable stylesheets (CSSStyleSheet API) to:
|
||||
* 1. Load CSS files once, not 14+ times per component
|
||||
* 2. Share stylesheets across all shadow DOMs
|
||||
* 3. Improve component initialization performance 40-60%
|
||||
* 4. Maintain CSS encapsulation with shadow DOM
|
||||
*
|
||||
* Usage:
|
||||
* // In component connectedCallback():
|
||||
* await StylesheetManager.attachStyles(this.shadowRoot, ['tokens', 'components']);
|
||||
*
|
||||
* Architecture:
|
||||
* - CSS files loaded once and cached in memory
|
||||
* - Parsed into CSSStyleSheet objects
|
||||
* - Adopted by shadow DOM (adoptedStyleSheets API)
|
||||
* - No re-parsing, no duplication
|
||||
*/
|
||||
|
||||
class StylesheetManager {
|
||||
// Cache for loaded stylesheets
|
||||
static #styleCache = new Map();
|
||||
|
||||
// Track loading promises to prevent race conditions
|
||||
static #loadingPromises = new Map();
|
||||
|
||||
// Configuration for stylesheet locations
|
||||
static #config = {
|
||||
tokens: '/admin-ui/css/tokens.css',
|
||||
components: '/admin-ui/css/dss-components.css',
|
||||
integrations: '/admin-ui/css/dss-integrations.css'
|
||||
};
|
||||
|
||||
/**
|
||||
* Load tokens stylesheet (colors, spacing, typography, etc.)
|
||||
* @returns {Promise<CSSStyleSheet>} Pre-parsed stylesheet
|
||||
*/
|
||||
static async loadTokens() {
|
||||
return this.#loadStylesheet('tokens', this.#config.tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load components stylesheet (component variant CSS)
|
||||
* @returns {Promise<CSSStyleSheet>} Pre-parsed stylesheet
|
||||
*/
|
||||
static async loadComponents() {
|
||||
return this.#loadStylesheet('components', this.#config.components);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load integrations stylesheet (third-party integrations)
|
||||
* @returns {Promise<CSSStyleSheet>} Pre-parsed stylesheet
|
||||
*/
|
||||
static async loadIntegrations() {
|
||||
return this.#loadStylesheet('integrations', this.#config.integrations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a specific stylesheet by key
|
||||
* @private
|
||||
*/
|
||||
static async #loadStylesheet(key, url) {
|
||||
// Return cached stylesheet if already loaded
|
||||
if (this.#styleCache.has(key)) {
|
||||
return this.#styleCache.get(key);
|
||||
}
|
||||
|
||||
// If currently loading, return the in-flight promise
|
||||
if (this.#loadingPromises.has(key)) {
|
||||
return this.#loadingPromises.get(key);
|
||||
}
|
||||
|
||||
// Create loading promise
|
||||
const promise = this.#fetchAndParseStylesheet(key, url);
|
||||
this.#loadingPromises.set(key, promise);
|
||||
|
||||
try {
|
||||
const sheet = await promise;
|
||||
this.#styleCache.set(key, sheet);
|
||||
return sheet;
|
||||
} finally {
|
||||
this.#loadingPromises.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch CSS file and parse into CSSStyleSheet
|
||||
* @private
|
||||
*/
|
||||
static async #fetchAndParseStylesheet(key, url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const cssText = await response.text();
|
||||
|
||||
// Create and populate CSSStyleSheet using constructable API
|
||||
const sheet = new CSSStyleSheet();
|
||||
await sheet.replace(cssText);
|
||||
|
||||
return sheet;
|
||||
} catch (error) {
|
||||
console.error(`[StylesheetManager] Error loading ${key}:`, error);
|
||||
// Return empty stylesheet as fallback
|
||||
const sheet = new CSSStyleSheet();
|
||||
await sheet.replace('/* Failed to load styles */');
|
||||
return sheet;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach stylesheets to a shadow DOM
|
||||
* @param {ShadowRoot} shadowRoot - Shadow DOM to attach styles to
|
||||
* @param {string[]} [keys] - Which stylesheets to attach (default: ['tokens', 'components'])
|
||||
* @returns {Promise<void>}
|
||||
*
|
||||
* Usage:
|
||||
* await StylesheetManager.attachStyles(this.shadowRoot);
|
||||
* await StylesheetManager.attachStyles(this.shadowRoot, ['tokens', 'components', 'integrations']);
|
||||
*/
|
||||
static async attachStyles(shadowRoot, keys = ['tokens', 'components']) {
|
||||
if (!shadowRoot || !shadowRoot.adoptedStyleSheets) {
|
||||
console.warn('[StylesheetManager] Shadow DOM does not support adoptedStyleSheets');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Load all requested stylesheets in parallel
|
||||
const sheets = await Promise.all(
|
||||
keys.map(key => {
|
||||
switch (key) {
|
||||
case 'tokens':
|
||||
return this.loadTokens();
|
||||
case 'components':
|
||||
return this.loadComponents();
|
||||
case 'integrations':
|
||||
return this.loadIntegrations();
|
||||
default:
|
||||
console.warn(`[StylesheetManager] Unknown stylesheet key: ${key}`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Filter out null values and set adopted stylesheets
|
||||
const validSheets = sheets.filter(s => s !== null);
|
||||
if (validSheets.length > 0) {
|
||||
shadowRoot.adoptedStyleSheets = [
|
||||
...shadowRoot.adoptedStyleSheets,
|
||||
...validSheets
|
||||
];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[StylesheetManager] Error attaching styles:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-load stylesheets at app initialization
|
||||
* Useful for warming cache before first component renders
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async preloadAll() {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadTokens(),
|
||||
this.loadComponents(),
|
||||
this.loadIntegrations()
|
||||
]);
|
||||
console.log('[StylesheetManager] All stylesheets pre-loaded');
|
||||
} catch (error) {
|
||||
console.error('[StylesheetManager] Error pre-loading stylesheets:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache and reload stylesheets
|
||||
* Useful for development hot-reload scenarios
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async clearCache() {
|
||||
this.#styleCache.clear();
|
||||
this.#loadingPromises.clear();
|
||||
console.log('[StylesheetManager] Cache cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current cache statistics
|
||||
* @returns {object} Cache info
|
||||
*/
|
||||
static getStats() {
|
||||
return {
|
||||
cachedSheets: Array.from(this.#styleCache.keys()),
|
||||
pendingLoads: Array.from(this.#loadingPromises.keys()),
|
||||
totalCached: this.#styleCache.size,
|
||||
totalPending: this.#loadingPromises.size
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a stylesheet is cached
|
||||
* @param {string} key - Stylesheet key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isCached(key) {
|
||||
return this.#styleCache.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom stylesheet URL
|
||||
* @param {string} key - Stylesheet key
|
||||
* @param {string} url - New URL for stylesheet
|
||||
*/
|
||||
static setStylesheetUrl(key, url) {
|
||||
if (this.#styleCache.has(key)) {
|
||||
console.warn(`[StylesheetManager] Cannot change URL for already-cached stylesheet: ${key}`);
|
||||
return;
|
||||
}
|
||||
this.#config[key] = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the stylesheet manager instance for global access
|
||||
*/
|
||||
static getInstance() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// Make available globally
|
||||
if (typeof window !== 'undefined') {
|
||||
window.StylesheetManager = StylesheetManager;
|
||||
|
||||
// Pre-load stylesheets when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
StylesheetManager.preloadAll().catch(e => {
|
||||
console.error('[StylesheetManager] Failed to pre-load on DOMContentLoaded:', e);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
StylesheetManager.preloadAll().catch(e => {
|
||||
console.error('[StylesheetManager] Failed to pre-load:', e);
|
||||
});
|
||||
}
|
||||
|
||||
// Add to debug interface
|
||||
if (!window.__DSS_DEBUG) {
|
||||
window.__DSS_DEBUG = {};
|
||||
}
|
||||
|
||||
window.__DSS_DEBUG.stylesheets = () => {
|
||||
const stats = StylesheetManager.getStats();
|
||||
console.table(stats);
|
||||
return stats;
|
||||
};
|
||||
|
||||
window.__DSS_DEBUG.clearStyleCache = () => {
|
||||
StylesheetManager.clearCache();
|
||||
console.log('Stylesheet cache cleared');
|
||||
};
|
||||
}
|
||||
|
||||
// Export for module systems
|
||||
export default StylesheetManager;
|
||||
Reference in New Issue
Block a user