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