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
362 lines
9.4 KiB
JavaScript
362 lines
9.4 KiB
JavaScript
/**
|
|
* table-template.js
|
|
* DSS-compliant table template with expandable rows
|
|
*
|
|
* Replaces inline event handlers and styles for table-based UIs
|
|
* - NO inline event handlers (onmouseover, onmouseout, onclick)
|
|
* - NO inline styles (uses Shadow DOM <style> blocks)
|
|
* - CSS :hover for row highlighting
|
|
* - data-action attributes for event delegation
|
|
* - Expandable detail rows pattern
|
|
*
|
|
* Reference: .knowledge/dss-coding-standards.json
|
|
*/
|
|
|
|
import { ComponentHelpers } from '../utils/component-helpers.js';
|
|
|
|
/**
|
|
* Create an expandable table view (DSS-compliant)
|
|
*
|
|
* @param {Object} config - Table configuration
|
|
* @param {string} config.title - Table title (optional)
|
|
* @param {Array} config.columns - Column definitions [{header, key, width, align}]
|
|
* @param {Array} config.rows - Row data objects
|
|
* @param {Function} config.renderCell - Custom cell renderer (column, row) => html
|
|
* @param {Function} config.renderDetails - Detail row renderer (row) => html
|
|
* @param {string} [config.emptyMessage] - Message when no rows
|
|
* @param {string} [config.emptyIcon] - Icon for empty state
|
|
* @returns {Object} {html, styles} - Separated HTML and CSS for Shadow DOM
|
|
*/
|
|
export function createTableView(config) {
|
|
const {
|
|
title,
|
|
columns = [],
|
|
rows = [],
|
|
renderCell,
|
|
renderDetails,
|
|
emptyMessage = 'No data available',
|
|
emptyIcon = '📋'
|
|
} = config;
|
|
|
|
// Validation
|
|
if (!Array.isArray(columns) || columns.length === 0) {
|
|
throw new Error('createTableView requires columns array');
|
|
}
|
|
if (!Array.isArray(rows)) {
|
|
throw new Error('createTableView requires rows array');
|
|
}
|
|
|
|
// Generate HTML (NO inline styles, NO inline event handlers)
|
|
const html = `
|
|
<div class="table-container">
|
|
${title ? `
|
|
<div class="table-header">
|
|
<h2 class="table-title">${ComponentHelpers.escapeHtml(title)}</h2>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="table-body">
|
|
${rows.length === 0 ? renderEmpty(emptyMessage, emptyIcon) : renderTable(columns, rows, renderCell, renderDetails)}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Separate styles for Shadow DOM <style> block
|
|
const styles = `
|
|
/* Table Container */
|
|
.table-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Header */
|
|
.table-header {
|
|
padding: 16px;
|
|
border-bottom: 1px solid var(--vscode-panel-border);
|
|
}
|
|
|
|
.table-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
color: var(--vscode-foreground);
|
|
}
|
|
|
|
/* Body */
|
|
.table-body {
|
|
flex: 1;
|
|
overflow: auto;
|
|
}
|
|
|
|
/* Table */
|
|
.data-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
background-color: var(--vscode-sideBar-background);
|
|
}
|
|
|
|
/* Table Header */
|
|
.data-table thead tr {
|
|
border-bottom: 2px solid var(--vscode-panel-border);
|
|
}
|
|
|
|
.data-table th {
|
|
padding: 8px 12px;
|
|
text-align: left;
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: var(--vscode-descriptionForeground);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Table Body Rows */
|
|
.data-table tbody tr.table-row {
|
|
border-bottom: 1px solid var(--vscode-panel-border);
|
|
cursor: pointer;
|
|
transition: background-color 0.15s ease;
|
|
}
|
|
|
|
/* STYLE-003: CSS :hover instead of inline onmouseover/onmouseout */
|
|
.data-table tbody tr.table-row:hover {
|
|
background-color: var(--vscode-list-hoverBackground);
|
|
}
|
|
|
|
.data-table tbody tr.table-row:active {
|
|
background-color: var(--vscode-list-activeSelectionBackground);
|
|
}
|
|
|
|
.data-table td {
|
|
padding: 8px 12px;
|
|
font-size: 11px;
|
|
color: var(--vscode-foreground);
|
|
}
|
|
|
|
/* Detail Rows */
|
|
.data-table tbody tr.detail-row {
|
|
background-color: var(--vscode-editor-background);
|
|
display: none;
|
|
}
|
|
|
|
.data-table tbody tr.detail-row.visible {
|
|
display: table-row;
|
|
}
|
|
|
|
.detail-content {
|
|
padding: 16px;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.detail-content > div {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.detail-label {
|
|
color: var(--vscode-descriptionForeground);
|
|
display: inline-block;
|
|
width: 100px;
|
|
}
|
|
|
|
.detail-value {
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
|
|
.detail-code {
|
|
background-color: var(--vscode-sideBar-background);
|
|
padding: 8px;
|
|
border-radius: 2px;
|
|
overflow-x: auto;
|
|
font-size: 10px;
|
|
font-family: 'Courier New', monospace;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Empty State */
|
|
.table-empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 48px 16px;
|
|
color: var(--vscode-descriptionForeground);
|
|
}
|
|
|
|
.table-empty-icon {
|
|
font-size: 48px;
|
|
margin-bottom: 12px;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.table-empty-text {
|
|
font-size: 13px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Column Alignment */
|
|
.align-left { text-align: left; }
|
|
.align-center { text-align: center; }
|
|
.align-right { text-align: right; }
|
|
|
|
/* Expand/Collapse Indicator */
|
|
.expand-indicator {
|
|
display: inline-block;
|
|
margin-right: 8px;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.table-row.expanded .expand-indicator {
|
|
transform: rotate(90deg);
|
|
}
|
|
`;
|
|
|
|
return { html, styles };
|
|
}
|
|
|
|
/**
|
|
* Render empty state
|
|
* @private
|
|
*/
|
|
function renderEmpty(message, icon) {
|
|
return `
|
|
<div class="table-empty">
|
|
<div class="table-empty-icon">${icon}</div>
|
|
<div class="table-empty-text">${ComponentHelpers.escapeHtml(message)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Render table with rows
|
|
* @private
|
|
*/
|
|
function renderTable(columns, rows, renderCell, renderDetails) {
|
|
return `
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
${renderDetails ? '<th style="width: 24px;"></th>' : ''}
|
|
${columns.map(col => `
|
|
<th class="align-${col.align || 'left'}" ${col.width ? `style="width: ${col.width};"` : ''}>
|
|
${ComponentHelpers.escapeHtml(col.header)}
|
|
</th>
|
|
`).join('')}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${rows.map((row, idx) => renderRow(columns, row, idx, renderCell, renderDetails)).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Render individual table row with optional expandable details
|
|
* EVENT-002: Use data-action attributes for event delegation
|
|
* @private
|
|
*/
|
|
function renderRow(columns, row, idx, renderCell, renderDetails) {
|
|
const mainRow = `
|
|
<tr class="table-row" data-action="row-click" data-row-idx="${idx}">
|
|
${renderDetails ? `
|
|
<td>
|
|
<span class="expand-indicator">▶</span>
|
|
</td>
|
|
` : ''}
|
|
${columns.map(col => `
|
|
<td class="align-${col.align || 'left'}">
|
|
${renderCell ? renderCell(col, row, idx) : renderDefaultCell(col, row)}
|
|
</td>
|
|
`).join('')}
|
|
</tr>
|
|
`;
|
|
|
|
const detailRow = renderDetails ? `
|
|
<tr class="detail-row" data-row-idx="${idx}">
|
|
<td colspan="${columns.length + 1}">
|
|
<div class="detail-content">
|
|
${renderDetails(row, idx)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
` : '';
|
|
|
|
return mainRow + detailRow;
|
|
}
|
|
|
|
/**
|
|
* Default cell renderer
|
|
* @private
|
|
*/
|
|
function renderDefaultCell(col, row) {
|
|
const value = row[col.key];
|
|
|
|
if (value === null || value === undefined) {
|
|
return '<span style="color: var(--vscode-descriptionForeground);">-</span>';
|
|
}
|
|
|
|
if (typeof value === 'boolean') {
|
|
return value ? '✓' : '✗';
|
|
}
|
|
|
|
return ComponentHelpers.escapeHtml(String(value));
|
|
}
|
|
|
|
/**
|
|
* Setup event handlers for table interactions
|
|
* To be called by the component after rendering
|
|
*
|
|
* @param {ShadowRoot} shadowRoot - Component's shadow root
|
|
* @param {Function} onRowClick - Handler for row clicks (optional)
|
|
*/
|
|
export function setupTableEvents(shadowRoot, onRowClick) {
|
|
const tableBody = shadowRoot.querySelector('.data-table tbody');
|
|
if (!tableBody) return;
|
|
|
|
// EVENT-002: Event delegation pattern
|
|
tableBody.addEventListener('click', (e) => {
|
|
const row = e.target.closest('[data-action="row-click"]');
|
|
if (!row) return;
|
|
|
|
const idx = parseInt(row.dataset.rowIdx, 10);
|
|
if (isNaN(idx)) return;
|
|
|
|
// Toggle detail row visibility
|
|
const detailRow = shadowRoot.querySelector(`.detail-row[data-row-idx="${idx}"]`);
|
|
if (detailRow) {
|
|
const isVisible = detailRow.classList.toggle('visible');
|
|
row.classList.toggle('expanded', isVisible);
|
|
}
|
|
|
|
// Call custom handler if provided
|
|
if (onRowClick) {
|
|
onRowClick(idx, e);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper: Create a stats summary card for tables
|
|
* @param {Object} stats - Stats object {label: value}
|
|
* @returns {string} HTML string
|
|
*/
|
|
export function createStatsCard(stats) {
|
|
const entries = Object.entries(stats);
|
|
|
|
return `
|
|
<div style="background-color: var(--vscode-sideBar-background); border: 1px solid var(--vscode-panel-border); border-radius: 4px; padding: 12px; margin-bottom: 16px;">
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 12px; font-size: 11px;">
|
|
${entries.map(([label, value]) => `
|
|
<div style="text-align: center;">
|
|
<div style="font-size: 20px; font-weight: 600; color: var(--vscode-foreground);">${ComponentHelpers.escapeHtml(String(value))}</div>
|
|
<div style="color: var(--vscode-descriptionForeground); margin-top: 2px;">${ComponentHelpers.escapeHtml(label)}</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
export default createTableView;
|