Files
dss/admin-ui/js/templates/table-template.js
Digital Production Factory 276ed71f31 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
2025-12-09 18:45:48 -03:00

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;