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
321 lines
8.2 KiB
JavaScript
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;
|