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
547 lines
20 KiB
JavaScript
547 lines
20 KiB
JavaScript
/**
|
|
* tool-templates.js
|
|
* Reusable template functions for building team-specific tool components
|
|
* Follows DRY principles to avoid code duplication across 14 team tools
|
|
*/
|
|
|
|
import { ComponentHelpers } from './component-helpers.js';
|
|
import toolBridge from '../services/tool-bridge.js';
|
|
|
|
/**
|
|
* Create a side-by-side comparison view
|
|
* Used for: Storybook/Figma, Storybook/Live, Figma/Live comparisons
|
|
*
|
|
* @param {Object} config
|
|
* @param {string} config.leftTitle - Title for left panel
|
|
* @param {string} config.rightTitle - Title for right panel
|
|
* @param {string} config.leftSrc - URL or content for left panel
|
|
* @param {string} config.rightSrc - URL or content for right panel
|
|
* @param {Function} config.onSync - Optional sync scroll callback
|
|
* @returns {string} HTML template
|
|
*/
|
|
export function createComparisonView(config) {
|
|
const {
|
|
leftTitle = 'Left',
|
|
rightTitle = 'Right',
|
|
leftSrc = '',
|
|
rightSrc = '',
|
|
onSync = null
|
|
} = config;
|
|
|
|
return `
|
|
<div style="display: flex; flex-direction: column; height: 100%;">
|
|
<!-- Toolbar -->
|
|
<div style="padding: 12px 16px; border-bottom: 1px solid var(--vscode-border); display: flex; justify-content: space-between; align-items: center;">
|
|
<div style="display: flex; gap: 12px; align-items: center;">
|
|
<button id="sync-scroll-btn" class="button" style="font-size: 11px; padding: 4px 12px;">
|
|
🔗 Sync Scroll
|
|
</button>
|
|
<button id="reset-zoom-btn" class="button" style="font-size: 11px; padding: 4px 12px;">
|
|
🔍 Reset Zoom
|
|
</button>
|
|
</div>
|
|
<div style="font-size: 11px; color: var(--vscode-text-dim);">
|
|
Use mouse wheel to zoom, drag to pan
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comparison Panels -->
|
|
<div style="flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--vscode-border); overflow: hidden;">
|
|
<!-- Left Panel -->
|
|
<div style="background: var(--vscode-bg); display: flex; flex-direction: column;">
|
|
<div style="padding: 8px 12px; background: var(--vscode-sidebar); border-bottom: 1px solid var(--vscode-border); font-size: 12px; font-weight: 600;">
|
|
${ComponentHelpers.escapeHtml(leftTitle)}
|
|
</div>
|
|
<div id="left-panel-content" style="flex: 1; overflow: auto; position: relative;">
|
|
${leftSrc ? `<iframe src="${ComponentHelpers.escapeHtml(leftSrc)}" style="width: 100%; height: 100%; border: none;"></iframe>` : ComponentHelpers.renderEmpty('Select content to display', '📄')}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Panel -->
|
|
<div style="background: var(--vscode-bg); display: flex; flex-direction: column;">
|
|
<div style="padding: 8px 12px; background: var(--vscode-sidebar); border-bottom: 1px solid var(--vscode-border); font-size: 12px; font-weight: 600;">
|
|
${ComponentHelpers.escapeHtml(rightTitle)}
|
|
</div>
|
|
<div id="right-panel-content" style="flex: 1; overflow: auto; position: relative;">
|
|
${rightSrc ? `<iframe src="${ComponentHelpers.escapeHtml(rightSrc)}" style="width: 100%; height: 100%; border: none;"></iframe>` : ComponentHelpers.renderEmpty('Select content to display', '📄')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Create a list view with search, filter, and actions
|
|
* Used for: Token list, Asset list, Component list
|
|
*
|
|
* @param {Object} config
|
|
* @param {string} config.title - List title
|
|
* @param {Array} config.items - Array of items to display
|
|
* @param {Array} config.columns - Column definitions [{ key, label, render }]
|
|
* @param {Array} config.actions - Action buttons [{ label, icon, onClick }]
|
|
* @param {Function} config.onSearch - Search callback
|
|
* @param {Function} config.onFilter - Filter callback
|
|
* @returns {string} HTML template
|
|
*/
|
|
export function createListView(config) {
|
|
const {
|
|
title = 'Items',
|
|
items = [],
|
|
columns = [],
|
|
actions = [],
|
|
onSearch = null,
|
|
onFilter = null
|
|
} = config;
|
|
|
|
return `
|
|
<div style="display: flex; flex-direction: column; height: 100%;">
|
|
<!-- Header -->
|
|
<div style="padding: 16px; border-bottom: 1px solid var(--vscode-border);">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
|
<h2 style="font-size: 16px; font-weight: 600;">${ComponentHelpers.escapeHtml(title)}</h2>
|
|
<div style="display: flex; gap: 8px;">
|
|
${actions.map((action, idx) => `
|
|
<button class="action-btn button" data-action-idx="${idx}" style="font-size: 11px; padding: 4px 12px;">
|
|
${action.icon || ''} ${ComponentHelpers.escapeHtml(action.label)}
|
|
</button>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Bar -->
|
|
<div style="display: flex; gap: 8px;">
|
|
<input
|
|
type="text"
|
|
id="search-input"
|
|
placeholder="Search..."
|
|
class="input"
|
|
style="flex: 1; font-size: 12px;"
|
|
/>
|
|
<select id="filter-select" class="input" style="font-size: 12px; width: 150px;">
|
|
<option value="">All Types</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<div style="flex: 1; overflow: auto;">
|
|
${items.length === 0 ? ComponentHelpers.renderEmpty(`No ${title.toLowerCase()} found`, '📦') : `
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
|
|
<thead style="position: sticky; top: 0; background: var(--vscode-sidebar); z-index: 1;">
|
|
<tr>
|
|
${columns.map(col => `
|
|
<th style="padding: 8px 12px; text-align: left; border-bottom: 1px solid var(--vscode-border); font-weight: 600;">
|
|
${ComponentHelpers.escapeHtml(col.label)}
|
|
</th>
|
|
`).join('')}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${items.map((item, itemIdx) => `
|
|
<tr style="border-bottom: 1px solid var(--vscode-border);" onmouseover="this.style.background='var(--vscode-list-hoverBackground)'" onmouseout="this.style.background='transparent'">
|
|
${columns.map(col => `
|
|
<td style="padding: 8px 12px;">
|
|
${col.render ? col.render(item) : ComponentHelpers.escapeHtml(String(item[col.key] || ''))}
|
|
</td>
|
|
`).join('')}
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`}
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div style="padding: 12px 16px; border-top: 1px solid var(--vscode-border); font-size: 11px; color: var(--vscode-text-dim);">
|
|
Showing ${items.length} ${title.toLowerCase()}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Create an editor view with save/export functionality
|
|
* Used for: ESRE editor, configuration editors
|
|
*
|
|
* @param {Object} config
|
|
* @param {string} config.title - Editor title
|
|
* @param {string} config.content - Initial content
|
|
* @param {string} config.language - Syntax highlighting language (text, json, yaml, etc.)
|
|
* @param {Function} config.onSave - Save callback
|
|
* @param {Function} config.onExport - Export callback
|
|
* @returns {string} HTML template
|
|
*/
|
|
export function createEditorView(config) {
|
|
const {
|
|
title = 'Editor',
|
|
content = '',
|
|
language = 'text',
|
|
onSave = null,
|
|
onExport = null
|
|
} = config;
|
|
|
|
return `
|
|
<div style="display: flex; flex-direction: column; height: 100%;">
|
|
<!-- Header -->
|
|
<div style="padding: 12px 16px; border-bottom: 1px solid var(--vscode-border); display: flex; justify-content: space-between; align-items: center;">
|
|
<h2 style="font-size: 14px; font-weight: 600;">${ComponentHelpers.escapeHtml(title)}</h2>
|
|
<div style="display: flex; gap: 8px;">
|
|
<button id="editor-save-btn" class="button" style="font-size: 11px; padding: 4px 12px;">
|
|
💾 Save
|
|
</button>
|
|
<button id="editor-export-btn" class="button" style="font-size: 11px; padding: 4px 12px;">
|
|
📥 Export
|
|
</button>
|
|
<button id="editor-clear-btn" class="button" style="font-size: 11px; padding: 4px 12px;">
|
|
🗑️ Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Editor -->
|
|
<div style="flex: 1; overflow: hidden; position: relative;">
|
|
<textarea
|
|
id="editor-content"
|
|
class="input"
|
|
style="width: 100%; height: 100%; resize: none; font-family: 'Courier New', monospace; font-size: 12px; padding: 16px; border: none;"
|
|
placeholder="Enter content here..."
|
|
>${ComponentHelpers.escapeHtml(content)}</textarea>
|
|
</div>
|
|
|
|
<!-- Footer Stats -->
|
|
<div style="padding: 8px 16px; border-top: 1px solid var(--vscode-border); display: flex; justify-content: space-between; font-size: 10px; color: var(--vscode-text-dim);">
|
|
<span id="editor-stats">0 lines, 0 characters</span>
|
|
<span>Language: ${language}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Create a gallery/grid view for visual content
|
|
* Used for: Screenshot gallery, navigation demos
|
|
*
|
|
* @param {Object} config
|
|
* @param {string} config.title - Gallery title
|
|
* @param {Array} config.items - Array of items with { id, src, title, subtitle }
|
|
* @param {Function} config.onItemClick - Item click callback
|
|
* @param {Function} config.onDelete - Delete callback
|
|
* @returns {string} HTML template
|
|
*/
|
|
export function createGalleryView(config) {
|
|
const {
|
|
title = 'Gallery',
|
|
items = [],
|
|
onItemClick = null,
|
|
onDelete = null
|
|
} = config;
|
|
|
|
return `
|
|
<div style="display: flex; flex-direction: column; height: 100%;">
|
|
<!-- Header -->
|
|
<div style="padding: 16px; border-bottom: 1px solid var(--vscode-border);">
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
<h2 style="font-size: 16px; font-weight: 600;">${ComponentHelpers.escapeHtml(title)}</h2>
|
|
<div style="font-size: 11px; color: var(--vscode-text-dim);">
|
|
${items.length} ${items.length === 1 ? 'item' : 'items'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gallery Grid -->
|
|
<div style="flex: 1; overflow: auto; padding: 16px;">
|
|
${items.length === 0 ? ComponentHelpers.renderEmpty(`No ${title.toLowerCase()} available`, '🖼️') : `
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px;">
|
|
${items.map((item, idx) => `
|
|
<div class="gallery-item" data-item-idx="${idx}" style="
|
|
background: var(--vscode-sidebar);
|
|
border: 1px solid var(--vscode-border);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
" onmouseover="this.style.transform='translateY(-4px)'; this.style.boxShadow='0 4px 12px rgba(0,0,0,0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
|
|
<!-- Image/Preview -->
|
|
<div style="aspect-ratio: 16/9; background: var(--vscode-bg); display: flex; align-items: center; justify-content: center; overflow: hidden;">
|
|
${item.src ? `<img src="${ComponentHelpers.escapeHtml(item.src)}" style="width: 100%; height: 100%; object-fit: cover;" />` : '<div style="font-size: 48px;">📄</div>'}
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div style="padding: 12px;">
|
|
<div style="font-size: 12px; font-weight: 600; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
|
${ComponentHelpers.escapeHtml(item.title || 'Untitled')}
|
|
</div>
|
|
${item.subtitle ? `<div style="font-size: 10px; color: var(--vscode-text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${ComponentHelpers.escapeHtml(item.subtitle)}</div>` : ''}
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
${onDelete ? `
|
|
<div style="padding: 0 12px 12px;">
|
|
<button class="gallery-delete-btn button" data-item-idx="${idx}" style="width: 100%; font-size: 10px; padding: 4px;">
|
|
🗑️ Delete
|
|
</button>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Create a form view with validation
|
|
* Used for: Project analysis configuration, quick wins settings
|
|
*
|
|
* @param {Object} config
|
|
* @param {string} config.title - Form title
|
|
* @param {Array} config.fields - Field definitions [{ name, label, type, placeholder, required }]
|
|
* @param {Function} config.onSubmit - Submit callback
|
|
* @returns {string} HTML template
|
|
*/
|
|
export function createFormView(config) {
|
|
const {
|
|
title = 'Configuration',
|
|
fields = [],
|
|
onSubmit = null
|
|
} = config;
|
|
|
|
return `
|
|
<div style="padding: 24px; max-width: 600px; margin: 0 auto;">
|
|
<h2 style="font-size: 16px; font-weight: 600; margin-bottom: 24px;">${ComponentHelpers.escapeHtml(title)}</h2>
|
|
|
|
<form id="config-form" style="display: flex; flex-direction: column; gap: 16px;">
|
|
${fields.map(field => `
|
|
<div>
|
|
<label style="display: block; font-size: 12px; font-weight: 600; margin-bottom: 6px;">
|
|
${ComponentHelpers.escapeHtml(field.label)}
|
|
${field.required ? '<span style="color: #f48771;">*</span>' : ''}
|
|
</label>
|
|
${field.type === 'textarea' ? `
|
|
<textarea
|
|
name="${field.name}"
|
|
class="input"
|
|
placeholder="${ComponentHelpers.escapeHtml(field.placeholder || '')}"
|
|
style="width: 100%; min-height: 80px; font-size: 12px;"
|
|
${field.required ? 'required' : ''}
|
|
></textarea>
|
|
` : field.type === 'select' ? `
|
|
<select
|
|
name="${field.name}"
|
|
class="input"
|
|
style="width: 100%; font-size: 12px;"
|
|
${field.required ? 'required' : ''}
|
|
>
|
|
<option value="">Select...</option>
|
|
${(field.options || []).map(opt => `
|
|
<option value="${ComponentHelpers.escapeHtml(opt.value)}">${ComponentHelpers.escapeHtml(opt.label)}</option>
|
|
`).join('')}
|
|
</select>
|
|
` : `
|
|
<input
|
|
type="${field.type || 'text'}"
|
|
name="${field.name}"
|
|
class="input"
|
|
placeholder="${ComponentHelpers.escapeHtml(field.placeholder || '')}"
|
|
style="width: 100%; font-size: 12px;"
|
|
${field.required ? 'required' : ''}
|
|
/>
|
|
`}
|
|
${field.description ? `<div style="font-size: 10px; color: var(--vscode-text-dim); margin-top: 4px;">${ComponentHelpers.escapeHtml(field.description)}</div>` : ''}
|
|
</div>
|
|
`).join('')}
|
|
|
|
<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 12px;">
|
|
<button type="button" id="form-cancel-btn" class="button" style="font-size: 12px;">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" class="button" style="font-size: 12px;">
|
|
Submit
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Setup event handlers for comparison view
|
|
*/
|
|
export function setupComparisonHandlers(container, config) {
|
|
const syncBtn = container.querySelector('#sync-scroll-btn');
|
|
const resetBtn = container.querySelector('#reset-zoom-btn');
|
|
const leftPanel = container.querySelector('#left-panel-content');
|
|
const rightPanel = container.querySelector('#right-panel-content');
|
|
|
|
let syncEnabled = false;
|
|
|
|
if (syncBtn && leftPanel && rightPanel) {
|
|
syncBtn.addEventListener('click', () => {
|
|
syncEnabled = !syncEnabled;
|
|
syncBtn.textContent = syncEnabled ? '🔗 Synced' : '🔗 Sync Scroll';
|
|
|
|
if (syncEnabled) {
|
|
leftPanel.addEventListener('scroll', syncScroll);
|
|
rightPanel.addEventListener('scroll', syncScroll);
|
|
} else {
|
|
leftPanel.removeEventListener('scroll', syncScroll);
|
|
rightPanel.removeEventListener('scroll', syncScroll);
|
|
}
|
|
});
|
|
|
|
function syncScroll(e) {
|
|
if (!syncEnabled) return;
|
|
const source = e.target;
|
|
const target = source === leftPanel ? rightPanel : leftPanel;
|
|
target.scrollTop = source.scrollTop;
|
|
target.scrollLeft = source.scrollLeft;
|
|
}
|
|
}
|
|
|
|
if (resetBtn) {
|
|
resetBtn.addEventListener('click', () => {
|
|
const iframes = container.querySelectorAll('iframe');
|
|
iframes.forEach(iframe => {
|
|
iframe.style.transform = 'scale(1)';
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup event handlers for list view
|
|
*/
|
|
export function setupListHandlers(container, config) {
|
|
const searchInput = container.querySelector('#search-input');
|
|
const filterSelect = container.querySelector('#filter-select');
|
|
const actionBtns = container.querySelectorAll('.action-btn');
|
|
|
|
if (searchInput && config.onSearch) {
|
|
searchInput.addEventListener('input', (e) => {
|
|
config.onSearch(e.target.value);
|
|
});
|
|
}
|
|
|
|
if (filterSelect && config.onFilter) {
|
|
filterSelect.addEventListener('change', (e) => {
|
|
config.onFilter(e.target.value);
|
|
});
|
|
}
|
|
|
|
if (actionBtns && config.actions) {
|
|
actionBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const idx = parseInt(btn.dataset.actionIdx);
|
|
if (config.actions[idx] && config.actions[idx].onClick) {
|
|
config.actions[idx].onClick();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup event handlers for editor view
|
|
*/
|
|
export function setupEditorHandlers(container, config) {
|
|
const saveBtn = container.querySelector('#editor-save-btn');
|
|
const exportBtn = container.querySelector('#editor-export-btn');
|
|
const clearBtn = container.querySelector('#editor-clear-btn');
|
|
const textarea = container.querySelector('#editor-content');
|
|
const stats = container.querySelector('#editor-stats');
|
|
|
|
function updateStats() {
|
|
if (textarea && stats) {
|
|
const lines = textarea.value.split('\n').length;
|
|
const chars = textarea.value.length;
|
|
stats.textContent = `${lines} lines, ${chars} characters`;
|
|
}
|
|
}
|
|
|
|
if (textarea) {
|
|
textarea.addEventListener('input', updateStats);
|
|
updateStats();
|
|
}
|
|
|
|
if (saveBtn && config.onSave) {
|
|
saveBtn.addEventListener('click', () => {
|
|
config.onSave(textarea.value);
|
|
});
|
|
}
|
|
|
|
if (exportBtn && config.onExport) {
|
|
exportBtn.addEventListener('click', () => {
|
|
config.onExport(textarea.value);
|
|
});
|
|
}
|
|
|
|
if (clearBtn && textarea) {
|
|
clearBtn.addEventListener('click', () => {
|
|
if (confirm('Clear all content?')) {
|
|
textarea.value = '';
|
|
updateStats();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup event handlers for gallery view
|
|
*/
|
|
export function setupGalleryHandlers(container, config) {
|
|
const items = container.querySelectorAll('.gallery-item');
|
|
const deleteBtns = container.querySelectorAll('.gallery-delete-btn');
|
|
|
|
if (items && config.onItemClick) {
|
|
items.forEach(item => {
|
|
item.addEventListener('click', (e) => {
|
|
// Don't trigger if delete button was clicked
|
|
if (e.target.classList.contains('gallery-delete-btn')) return;
|
|
|
|
const idx = parseInt(item.dataset.itemIdx);
|
|
config.onItemClick(config.items[idx], idx);
|
|
});
|
|
});
|
|
}
|
|
|
|
if (deleteBtns && config.onDelete) {
|
|
deleteBtns.forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const idx = parseInt(btn.dataset.itemIdx);
|
|
if (confirm('Delete this item?')) {
|
|
config.onDelete(config.items[idx], idx);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup event handlers for form view
|
|
*/
|
|
export function setupFormHandlers(container, config) {
|
|
const form = container.querySelector('#config-form');
|
|
const cancelBtn = container.querySelector('#form-cancel-btn');
|
|
|
|
if (form && config.onSubmit) {
|
|
form.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
|
|
const formData = new FormData(form);
|
|
const data = Object.fromEntries(formData.entries());
|
|
|
|
config.onSubmit(data);
|
|
});
|
|
}
|
|
|
|
if (cancelBtn) {
|
|
cancelBtn.addEventListener('click', () => {
|
|
form.reset();
|
|
});
|
|
}
|
|
}
|