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:
334
admin-ui/js/utils/component-helpers.js
Normal file
334
admin-ui/js/utils/component-helpers.js
Normal file
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* component-helpers.js
|
||||
* Shared utilities for panel components
|
||||
* Provides standardized loading, error, and empty states
|
||||
*/
|
||||
|
||||
export class ComponentHelpers {
|
||||
/**
|
||||
* Render a loading spinner with optional message
|
||||
* @param {string} message - Optional loading message
|
||||
* @returns {string} HTML string for loading state
|
||||
*/
|
||||
static renderLoading(message = 'Loading...') {
|
||||
return `
|
||||
<div style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
color: var(--vscode-text-dim);
|
||||
">
|
||||
<div style="
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid var(--vscode-border);
|
||||
border-top-color: var(--vscode-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
"></div>
|
||||
<div style="margin-top: 16px; font-size: 12px;">${message}</div>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an error state with message
|
||||
* @param {string} message - Error message to display
|
||||
* @param {Error} error - Optional error object for details
|
||||
* @returns {string} HTML string for error state
|
||||
*/
|
||||
static renderError(message, error = null) {
|
||||
const details = error ? `<div style="
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background-color: rgba(244, 135, 113, 0.1);
|
||||
border-radius: 2px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
color: #f48771;
|
||||
">${error.message || error.toString()}</div>` : '';
|
||||
|
||||
return `
|
||||
<div style="
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #f48771;
|
||||
">
|
||||
<div style="font-size: 32px; margin-bottom: 12px;">⚠️</div>
|
||||
<div style="font-size: 13px; font-weight: 500;">${message}</div>
|
||||
${details}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an empty state with message
|
||||
* @param {string} message - Empty state message
|
||||
* @param {string} icon - Optional icon/emoji
|
||||
* @returns {string} HTML string for empty state
|
||||
*/
|
||||
static renderEmpty(message = 'No data available', icon = '📭') {
|
||||
return `
|
||||
<div style="
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
color: var(--vscode-text-dim);
|
||||
">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">${icon}</div>
|
||||
<div style="font-size: 13px;">${message}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp for display
|
||||
* @param {Date|string|number} date - Date to format
|
||||
* @param {boolean} includeTime - Include time in output
|
||||
* @returns {string} Formatted date string
|
||||
*/
|
||||
static formatTimestamp(date, includeTime = true) {
|
||||
if (!date) return 'N/A';
|
||||
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
|
||||
if (isNaN(d.getTime())) return 'Invalid Date';
|
||||
|
||||
const dateStr = d.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
if (!includeTime) return dateStr;
|
||||
|
||||
const timeStr = d.toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
|
||||
return `${dateStr} ${timeStr}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format relative time (e.g., "2 minutes ago")
|
||||
* @param {Date|string|number} date - Date to format
|
||||
* @returns {string} Relative time string
|
||||
*/
|
||||
static formatRelativeTime(date) {
|
||||
if (!date) return 'N/A';
|
||||
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
if (isNaN(d.getTime())) return 'Invalid Date';
|
||||
|
||||
const now = new Date();
|
||||
const diffMs = now - d;
|
||||
const diffSec = Math.floor(diffMs / 1000);
|
||||
const diffMin = Math.floor(diffSec / 60);
|
||||
const diffHour = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHour / 24);
|
||||
|
||||
if (diffSec < 60) return 'just now';
|
||||
if (diffMin < 60) return `${diffMin} minute${diffMin !== 1 ? 's' : ''} ago`;
|
||||
if (diffHour < 24) return `${diffHour} hour${diffHour !== 1 ? 's' : ''} ago`;
|
||||
if (diffDay < 7) return `${diffDay} day${diffDay !== 1 ? 's' : ''} ago`;
|
||||
|
||||
return this.formatTimestamp(d, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate text to max length with ellipsis
|
||||
* @param {string} text - Text to truncate
|
||||
* @param {number} maxLength - Maximum length
|
||||
* @returns {string} Truncated text
|
||||
*/
|
||||
static truncateText(text, maxLength = 100) {
|
||||
if (!text || text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
* @param {string} text - Text to escape
|
||||
* @returns {string} Escaped HTML
|
||||
*/
|
||||
static escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size in human-readable format
|
||||
* @param {number} bytes - Size in bytes
|
||||
* @returns {string} Formatted size string
|
||||
*/
|
||||
static formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
if (!bytes) return 'N/A';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration in human-readable format
|
||||
* @param {number} ms - Duration in milliseconds
|
||||
* @returns {string} Formatted duration string
|
||||
*/
|
||||
static formatDuration(ms) {
|
||||
if (ms === 0) return '0ms';
|
||||
if (!ms) return 'N/A';
|
||||
|
||||
if (ms < 1000) return `${Math.round(ms)}ms`;
|
||||
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
|
||||
if (ms < 3600000) return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
|
||||
|
||||
const hours = Math.floor(ms / 3600000);
|
||||
const minutes = Math.floor((ms % 3600000) / 60000);
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a badge element
|
||||
* @param {string} label - Badge label
|
||||
* @param {string} type - Badge type (success, warning, error, info)
|
||||
* @returns {string} HTML string for badge
|
||||
*/
|
||||
static createBadge(label, type = 'info') {
|
||||
const colors = {
|
||||
success: '#89d185',
|
||||
warning: '#dbb765',
|
||||
error: '#f48771',
|
||||
info: '#75beff'
|
||||
};
|
||||
|
||||
const color = colors[type] || colors.info;
|
||||
|
||||
return `
|
||||
<span style="
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background-color: rgba(${this.hexToRgb(color)}, 0.2);
|
||||
color: ${color};
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
">${label}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex color to RGB values
|
||||
* @param {string} hex - Hex color
|
||||
* @returns {string} RGB values (e.g., "255, 0, 0")
|
||||
*/
|
||||
static hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`
|
||||
: '128, 128, 128';
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce function execution
|
||||
* @param {Function} func - Function to debounce
|
||||
* @param {number} wait - Wait time in milliseconds
|
||||
* @returns {Function} Debounced function
|
||||
*/
|
||||
static debounce(func, wait = 300) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON safely with fallback
|
||||
* @param {string} jsonString - JSON string to parse
|
||||
* @param {*} fallback - Fallback value if parse fails
|
||||
* @returns {*} Parsed object or fallback
|
||||
*/
|
||||
static safeJsonParse(jsonString, fallback = null) {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse JSON:', e);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a table from array of objects
|
||||
* @param {Array<Object>} data - Array of objects
|
||||
* @param {Array<string>} columns - Column names to display
|
||||
* @returns {string} HTML string for table
|
||||
*/
|
||||
static createTable(data, columns = null) {
|
||||
if (!data || data.length === 0) {
|
||||
return this.renderEmpty('No data to display', '📋');
|
||||
}
|
||||
|
||||
const cols = columns || Object.keys(data[0]);
|
||||
|
||||
const headerRow = cols.map(col =>
|
||||
`<th style="
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--vscode-border);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--vscode-text-dim);
|
||||
">${col}</th>`
|
||||
).join('');
|
||||
|
||||
const dataRows = data.map(row => `
|
||||
<tr style="border-bottom: 1px solid var(--vscode-border);">
|
||||
${cols.map(col => `
|
||||
<td style="
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-text);
|
||||
">${this.escapeHtml(String(row[col] ?? ''))}</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
return `
|
||||
<div style="overflow-x: auto;">
|
||||
<table style="
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background-color: var(--vscode-sidebar);
|
||||
">
|
||||
<thead>
|
||||
<tr>${headerRow}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${dataRows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user