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:
361
admin-ui/js/templates/table-template.js
Normal file
361
admin-ui/js/templates/table-template.js
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user