Files
dss/admin-ui/js/templates/gallery-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

321 lines
8.2 KiB
JavaScript

/**
* gallery-template.js
* DSS-compliant gallery template module (Strangler Fig Pattern)
*
* Replaces createGalleryView() from tool-templates.js with standards-compliant version:
* - NO inline event handlers (onmouseover, onmouseout)
* - NO inline styles (uses Shadow DOM <style> blocks)
* - Uses data-action attributes for event delegation
* - Returns {html, styles} structure for Shadow DOM adoption
*
* Reference: .knowledge/dss-coding-standards.json
* - WC-001: Shadow DOM Required
* - EVENT-001: NO Inline Events
* - STYLE-001: NO Inline Styles
*/
import { ComponentHelpers } from '../utils/component-helpers.js';
/**
* Create a gallery view template (DSS-compliant)
*
* @param {Object} config - Gallery configuration
* @param {string} config.title - Gallery title
* @param {Array} config.items - Gallery items [{src, title, subtitle}]
* @param {Function} [config.onItemClick] - Click handler (handled via event delegation)
* @param {Function} [config.onDelete] - Delete handler (handled via event delegation)
* @returns {Object} {html, styles} - Separated HTML and CSS for Shadow DOM
*/
export function createGalleryView({ title, items = [], onItemClick = null, onDelete = null }) {
// Validation
if (!title || !Array.isArray(items)) {
throw new Error('createGalleryView requires title (string) and items (array)');
}
// Generate HTML (NO inline styles, NO inline event handlers)
const html = `
<div class="gallery-container">
<!-- Header -->
<div class="gallery-header">
<h2 class="gallery-title">${ComponentHelpers.escapeHtml(title)}</h2>
<div class="gallery-count">
${items.length} ${items.length === 1 ? 'item' : 'items'}
</div>
</div>
<!-- Gallery Grid -->
<div class="gallery-body">
${items.length === 0 ? renderEmpty(title) : renderGalleryGrid(items, onDelete)}
</div>
</div>
`;
// Separate styles for Shadow DOM <style> block
const styles = `
/* Gallery Container */
.gallery-container {
display: flex;
flex-direction: column;
height: 100%;
}
/* Header */
.gallery-header {
padding: 16px;
border-bottom: 1px solid var(--vscode-panel-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.gallery-title {
font-size: 16px;
font-weight: 600;
margin: 0;
color: var(--vscode-foreground);
}
.gallery-count {
font-size: 11px;
color: var(--vscode-descriptionForeground);
}
/* Body */
.gallery-body {
flex: 1;
overflow: auto;
padding: 16px;
}
/* Grid */
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
}
/* Gallery Item */
.gallery-item {
background: var(--vscode-sideBar-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
/* STYLE-003: CSS :hover instead of inline onmouseover/onmouseout */
.gallery-item:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.gallery-item:active {
transform: translateY(-2px);
}
/* Image/Preview */
.gallery-preview {
aspect-ratio: 16 / 9;
background: var(--vscode-editor-background);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.gallery-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.gallery-preview-placeholder {
font-size: 48px;
}
/* Info */
.gallery-info {
padding: 12px;
}
.gallery-item-title {
font-size: 12px;
font-weight: 600;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--vscode-foreground);
}
.gallery-item-subtitle {
font-size: 10px;
color: var(--vscode-descriptionForeground);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Actions */
.gallery-actions {
padding: 0 12px 12px;
}
.gallery-delete-btn {
width: 100%;
font-size: 10px;
padding: 4px 8px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
border-radius: 2px;
cursor: pointer;
transition: background 0.15s ease;
}
.gallery-delete-btn:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
.gallery-delete-btn:active {
background: var(--vscode-button-background);
}
/* Empty State */
.gallery-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 16px;
color: var(--vscode-descriptionForeground);
}
.gallery-empty-icon {
font-size: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.gallery-empty-text {
font-size: 13px;
text-align: center;
}
`;
return { html, styles };
}
/**
* Render empty state
* @private
*/
function renderEmpty(title) {
return `
<div class="gallery-empty">
<div class="gallery-empty-icon">🖼️</div>
<div class="gallery-empty-text">No ${title.toLowerCase()} available</div>
</div>
`;
}
/**
* Render gallery grid
* @private
*/
function renderGalleryGrid(items, onDelete) {
return `
<div class="gallery-grid">
${items.map((item, idx) => renderGalleryItem(item, idx, onDelete)).join('')}
</div>
`;
}
/**
* Render individual gallery item
* EVENT-002: Use data-action attributes for event delegation
* @private
*/
function renderGalleryItem(item, idx, onDelete) {
return `
<div class="gallery-item" data-action="item-click" data-item-idx="${idx}">
<!-- Image/Preview -->
<div class="gallery-preview">
${item.src
? `<img src="${ComponentHelpers.escapeHtml(item.src)}" alt="${ComponentHelpers.escapeHtml(item.title || 'Gallery item')}" />`
: '<div class="gallery-preview-placeholder">📄</div>'
}
</div>
<!-- Info -->
<div class="gallery-info">
<div class="gallery-item-title">
${ComponentHelpers.escapeHtml(item.title || 'Untitled')}
</div>
${item.subtitle ? `
<div class="gallery-item-subtitle">
${ComponentHelpers.escapeHtml(item.subtitle)}
</div>
` : ''}
</div>
<!-- Actions -->
${onDelete ? `
<div class="gallery-actions">
<button
class="gallery-delete-btn"
data-action="item-delete"
data-item-idx="${idx}"
type="button"
aria-label="Delete ${ComponentHelpers.escapeHtml(item.title || 'item')}">
🗑️ Delete
</button>
</div>
` : ''}
</div>
`;
}
/**
* Setup event handlers using event delegation pattern
* To be called by the component after rendering
*
* @param {ShadowRoot} shadowRoot - Component's shadow root
* @param {Function} onItemClick - Handler for item clicks
* @param {Function} onDelete - Handler for delete button clicks
*/
export function setupGalleryEvents(shadowRoot, onItemClick, onDelete) {
const container = shadowRoot.querySelector('.gallery-container');
if (!container) return;
// EVENT-002: Event delegation pattern
container.addEventListener('click', (e) => {
const target = e.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
const idx = parseInt(target.dataset.itemIdx, 10);
switch (action) {
case 'item-click':
if (onItemClick && !isNaN(idx)) {
// Don't trigger if clicking delete button
if (!e.target.closest('[data-action="item-delete"]')) {
onItemClick(idx, e);
}
}
break;
case 'item-delete':
if (onDelete && !isNaN(idx)) {
e.stopPropagation(); // Prevent item-click from firing
onDelete(idx, e);
}
break;
}
});
}
export default createGalleryView;