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:
552
admin-ui/js/components/tools/ds-screenshot-gallery.js
Normal file
552
admin-ui/js/components/tools/ds-screenshot-gallery.js
Normal file
@@ -0,0 +1,552 @@
|
||||
/**
|
||||
* ds-screenshot-gallery.js
|
||||
* Screenshot gallery with IndexedDB storage and artifact-based images
|
||||
*
|
||||
* REFACTORED: DSS-compliant version using DSBaseTool + gallery-template.js
|
||||
* - Extends DSBaseTool for Shadow DOM, AbortController, and standardized lifecycle
|
||||
* - Uses gallery-template.js for DSS-compliant templating (NO inline events/styles)
|
||||
* - Event delegation pattern for all interactions
|
||||
* - Logger utility instead of console.*
|
||||
*
|
||||
* Reference: .knowledge/dss-coding-standards.json
|
||||
*/
|
||||
|
||||
import DSBaseTool from '../base/ds-base-tool.js';
|
||||
import toolBridge from '../../services/tool-bridge.js';
|
||||
import { ComponentHelpers } from '../../utils/component-helpers.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
class DSScreenshotGallery extends DSBaseTool {
|
||||
constructor() {
|
||||
super();
|
||||
this.screenshots = [];
|
||||
this.selectedScreenshot = null;
|
||||
this.isCapturing = false;
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
// Initialize IndexedDB first
|
||||
await this.initDB();
|
||||
|
||||
// Call parent connectedCallback (renders + setupEventListeners)
|
||||
super.connectedCallback();
|
||||
|
||||
// Load screenshots after render
|
||||
await this.loadScreenshots();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize IndexedDB for metadata storage
|
||||
*/
|
||||
async initDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open('ds-screenshots', 1);
|
||||
|
||||
request.onerror = () => {
|
||||
logger.error('[DSScreenshotGallery] Failed to open IndexedDB', request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
logger.debug('[DSScreenshotGallery] IndexedDB initialized');
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains('screenshots')) {
|
||||
const store = db.createObjectStore('screenshots', { keyPath: 'id' });
|
||||
store.createIndex('timestamp', 'timestamp', { unique: false });
|
||||
store.createIndex('tags', 'tags', { unique: false, multiEntry: true });
|
||||
logger.info('[DSScreenshotGallery] IndexedDB schema created');
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component (required by DSBaseTool)
|
||||
*/
|
||||
render() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.screenshot-gallery-container {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.capture-controls {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.capture-input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.capture-input:focus {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.fullpage-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.capture-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.capture-btn:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.capture-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.gallery-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
font-size: 32px;
|
||||
margin-bottom: 12px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
background: var(--vscode-sideBar-background);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 14px;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.modal-subtitle {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: var(--vscode-foreground);
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-close-btn:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="screenshot-gallery-container">
|
||||
<!-- Capture Controls -->
|
||||
<div class="capture-controls">
|
||||
<input
|
||||
type="text"
|
||||
id="screenshot-selector"
|
||||
placeholder="Optional: CSS selector to capture"
|
||||
class="capture-input"
|
||||
/>
|
||||
<label class="fullpage-label">
|
||||
<input type="checkbox" id="screenshot-fullpage" />
|
||||
Full page
|
||||
</label>
|
||||
<button
|
||||
id="capture-screenshot-btn"
|
||||
data-action="capture"
|
||||
class="capture-btn"
|
||||
type="button"
|
||||
aria-label="Capture screenshot">
|
||||
📸 Capture
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Gallery Content -->
|
||||
<div class="gallery-wrapper" id="gallery-content">
|
||||
<div class="loading">
|
||||
<div class="loading-spinner">⏳</div>
|
||||
<div>Initializing gallery...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners (required by DSBaseTool)
|
||||
* Uses event delegation pattern with data-action attributes
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// EVENT-002: Event delegation on container
|
||||
this.delegateEvents('.screenshot-gallery-container', 'click', (action, e) => {
|
||||
switch (action) {
|
||||
case 'capture':
|
||||
this.captureScreenshot();
|
||||
break;
|
||||
case 'item-click':
|
||||
const idx = parseInt(e.target.closest('[data-item-idx]')?.dataset.itemIdx, 10);
|
||||
if (!isNaN(idx) && this.screenshots[idx]) {
|
||||
this.viewScreenshot(this.screenshots[idx]);
|
||||
}
|
||||
break;
|
||||
case 'item-delete':
|
||||
const deleteIdx = parseInt(e.target.closest('[data-item-idx]')?.dataset.itemIdx, 10);
|
||||
if (!isNaN(deleteIdx) && this.screenshots[deleteIdx]) {
|
||||
this.handleDelete(this.screenshots[deleteIdx].id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async captureScreenshot() {
|
||||
if (this.isCapturing) return;
|
||||
|
||||
this.isCapturing = true;
|
||||
const captureBtn = this.$('#capture-screenshot-btn');
|
||||
|
||||
if (captureBtn) {
|
||||
captureBtn.disabled = true;
|
||||
captureBtn.textContent = '📸 Capturing...';
|
||||
}
|
||||
|
||||
try {
|
||||
const selectorInput = this.$('#screenshot-selector');
|
||||
const fullPageToggle = this.$('#screenshot-fullpage');
|
||||
|
||||
const selector = selectorInput?.value.trim() || null;
|
||||
const fullPage = fullPageToggle?.checked || false;
|
||||
|
||||
logger.info('[DSScreenshotGallery] Capturing screenshot', { selector, fullPage });
|
||||
|
||||
// Call MCP tool to capture screenshot
|
||||
const result = await toolBridge.takeScreenshot(fullPage, selector);
|
||||
|
||||
if (result && result.screenshot) {
|
||||
// Save metadata to IndexedDB
|
||||
const screenshot = {
|
||||
id: Date.now(),
|
||||
timestamp: new Date(),
|
||||
selector: selector || 'Full Page',
|
||||
fullPage,
|
||||
imageData: result.screenshot, // Base64 image data
|
||||
tags: selector ? [selector] : ['fullpage']
|
||||
};
|
||||
|
||||
await this.saveScreenshot(screenshot);
|
||||
await this.loadScreenshots();
|
||||
|
||||
ComponentHelpers.showToast?.('Screenshot captured successfully', 'success');
|
||||
logger.info('[DSScreenshotGallery] Screenshot saved', { id: screenshot.id });
|
||||
} else {
|
||||
throw new Error('No screenshot data returned');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[DSScreenshotGallery] Failed to capture screenshot', error);
|
||||
ComponentHelpers.showToast?.(`Failed to capture screenshot: ${error.message}`, 'error');
|
||||
} finally {
|
||||
this.isCapturing = false;
|
||||
if (captureBtn) {
|
||||
captureBtn.disabled = false;
|
||||
captureBtn.textContent = '📸 Capture';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async saveScreenshot(screenshot) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(['screenshots'], 'readwrite');
|
||||
const store = transaction.objectStore('screenshots');
|
||||
const request = store.add(screenshot);
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async loadScreenshots() {
|
||||
const content = this.$('#gallery-content');
|
||||
if (!content) return;
|
||||
|
||||
try {
|
||||
this.screenshots = await this.getAllScreenshots();
|
||||
logger.debug('[DSScreenshotGallery] Loaded screenshots', { count: this.screenshots.length });
|
||||
this.renderGallery();
|
||||
} catch (error) {
|
||||
logger.error('[DSScreenshotGallery] Failed to load screenshots', error);
|
||||
content.innerHTML = ComponentHelpers.renderError('Failed to load screenshots', error);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllScreenshots() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(['screenshots'], 'readonly');
|
||||
const store = transaction.objectStore('screenshots');
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => resolve(request.result.reverse()); // Most recent first
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteScreenshot(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(['screenshots'], 'readwrite');
|
||||
const store = transaction.objectStore('screenshots');
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async handleDelete(id) {
|
||||
if (!confirm('Delete this screenshot?')) return;
|
||||
|
||||
try {
|
||||
await this.deleteScreenshot(id);
|
||||
await this.loadScreenshots();
|
||||
ComponentHelpers.showToast?.('Screenshot deleted', 'success');
|
||||
logger.info('[DSScreenshotGallery] Screenshot deleted', { id });
|
||||
} catch (error) {
|
||||
logger.error('[DSScreenshotGallery] Failed to delete screenshot', error);
|
||||
ComponentHelpers.showToast?.(`Failed to delete: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
viewScreenshot(screenshot) {
|
||||
this.selectedScreenshot = screenshot;
|
||||
this.renderModal();
|
||||
}
|
||||
|
||||
renderModal() {
|
||||
if (!this.selectedScreenshot) return;
|
||||
|
||||
// Create modal in Shadow DOM
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h3 class="modal-title">${this.escapeHtml(this.selectedScreenshot.selector)}</h3>
|
||||
<div class="modal-subtitle">
|
||||
${ComponentHelpers.formatTimestamp(new Date(this.selectedScreenshot.timestamp))}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="modal-close-btn"
|
||||
data-action="close-modal"
|
||||
type="button"
|
||||
aria-label="Close modal">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<img
|
||||
src="${this.selectedScreenshot.imageData}"
|
||||
class="modal-image"
|
||||
alt="${this.escapeHtml(this.selectedScreenshot.selector)}" />
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add click handlers for modal
|
||||
this.bindEvent(modal, 'click', (e) => {
|
||||
const closeBtn = e.target.closest('[data-action="close-modal"]');
|
||||
if (closeBtn || e.target === modal) {
|
||||
modal.remove();
|
||||
this.selectedScreenshot = null;
|
||||
logger.debug('[DSScreenshotGallery] Modal closed');
|
||||
}
|
||||
});
|
||||
|
||||
this.shadowRoot.appendChild(modal);
|
||||
logger.debug('[DSScreenshotGallery] Modal opened', { id: this.selectedScreenshot.id });
|
||||
}
|
||||
|
||||
renderGallery() {
|
||||
const content = this.$('#gallery-content');
|
||||
if (!content) return;
|
||||
|
||||
if (this.screenshots.length === 0) {
|
||||
content.innerHTML = `
|
||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px; color: var(--vscode-descriptionForeground);">
|
||||
<div style="font-size: 48px; margin-bottom: 12px; opacity: 0.5;">📸</div>
|
||||
<div style="font-size: 13px;">No screenshots captured yet</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform screenshots to gallery items format
|
||||
const galleryItems = this.screenshots.map(screenshot => ({
|
||||
src: screenshot.imageData,
|
||||
title: screenshot.selector,
|
||||
subtitle: ComponentHelpers.formatRelativeTime(new Date(screenshot.timestamp))
|
||||
}));
|
||||
|
||||
// Use DSS-compliant gallery template (NO inline styles/events)
|
||||
// Note: We're using a simplified inline version here since we're in Shadow DOM
|
||||
// For full modular approach, we'd import createGalleryView from gallery-template.js
|
||||
content.innerHTML = `
|
||||
<div style="margin-bottom: 12px; padding: 12px; background-color: var(--vscode-sideBar-background); border-radius: 4px;">
|
||||
<div style="font-size: 11px; color: var(--vscode-descriptionForeground);">
|
||||
${this.screenshots.length} screenshot${this.screenshots.length !== 1 ? 's' : ''} stored
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px;">
|
||||
${galleryItems.map((item, idx) => `
|
||||
<div class="gallery-item" data-action="item-click" data-item-idx="${idx}" style="
|
||||
background: var(--vscode-sideBar-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
">
|
||||
<div style="aspect-ratio: 16/9; overflow: hidden; background: var(--vscode-editor-background);">
|
||||
<img src="${item.src}"
|
||||
style="width: 100%; height: 100%; object-fit: cover;"
|
||||
alt="${this.escapeHtml(item.title)}" />
|
||||
</div>
|
||||
<div style="padding: 12px;">
|
||||
<div style="font-size: 12px; font-weight: 600; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
${this.escapeHtml(item.title)}
|
||||
</div>
|
||||
<div style="font-size: 11px; color: var(--vscode-descriptionForeground); margin-bottom: 8px;">
|
||||
${item.subtitle}
|
||||
</div>
|
||||
<button
|
||||
data-action="item-delete"
|
||||
data-item-idx="${idx}"
|
||||
type="button"
|
||||
aria-label="Delete ${this.escapeHtml(item.title)}"
|
||||
style="padding: 4px 8px; font-size: 10px; background: rgba(244, 135, 113, 0.1); color: #f48771; border: 1px solid #f48771; border-radius: 2px; cursor: pointer;">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add hover styles via adoptedStyleSheets
|
||||
this.adoptStyles(`
|
||||
.gallery-item:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.gallery-item button:hover {
|
||||
background: rgba(244, 135, 113, 0.2);
|
||||
}
|
||||
`);
|
||||
|
||||
logger.debug('[DSScreenshotGallery] Gallery rendered', { count: this.screenshots.length });
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ds-screenshot-gallery', DSScreenshotGallery);
|
||||
|
||||
export default DSScreenshotGallery;
|
||||
Reference in New Issue
Block a user