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:
Digital Production Factory
2025-12-09 18:45:48 -03:00
commit 276ed71f31
884 changed files with 373737 additions and 0 deletions

View File

@@ -0,0 +1,382 @@
/**
* ds-visual-diff.js
* Visual diff tool for comparing design changes using Pixelmatch
*/
import toolBridge from '../../services/tool-bridge.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
// Load Pixelmatch from CDN
let pixelmatch = null;
class DSVisualDiff extends HTMLElement {
constructor() {
super();
this.screenshots = [];
this.beforeImage = null;
this.afterImage = null;
this.diffResult = null;
this.isComparing = false;
this.pixelmatchLoaded = false;
}
async connectedCallback() {
this.render();
this.setupEventListeners();
await this.loadPixelmatch();
await this.loadScreenshots();
}
/**
* Load Pixelmatch library from CDN
*/
async loadPixelmatch() {
if (this.pixelmatchLoaded) return;
try {
// Import pixelmatch from esm.sh CDN
const module = await import('https://esm.sh/pixelmatch@5.3.0');
pixelmatch = module.default;
this.pixelmatchLoaded = true;
console.log('[DSVisualDiff] Pixelmatch loaded successfully');
} catch (error) {
console.error('[DSVisualDiff] Failed to load Pixelmatch:', error);
ComponentHelpers.showToast?.('Failed to load visual diff library', 'error');
}
}
/**
* Load screenshots from IndexedDB (shared with ds-screenshot-gallery)
*/
async loadScreenshots() {
try {
const db = await this.openDB();
this.screenshots = await this.getAllScreenshots(db);
this.renderSelectors();
} catch (error) {
console.error('Failed to load screenshots:', error);
}
}
async openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('ds-screenshots', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
async getAllScreenshots(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['screenshots'], 'readonly');
const store = transaction.objectStore('screenshots');
const request = store.getAll();
request.onsuccess = () => resolve(request.result.reverse());
request.onerror = () => reject(request.error);
});
}
setupEventListeners() {
const compareBtn = this.querySelector('#visual-diff-compare-btn');
if (compareBtn) {
compareBtn.addEventListener('click', () => this.compareImages());
}
}
/**
* Load image data and decode to ImageData
*/
async loadImageData(imageDataUrl) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
resolve(imageData);
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = imageDataUrl;
});
}
/**
* Compare two images using Pixelmatch
*/
async compareImages() {
if (this.isComparing || !this.pixelmatchLoaded) return;
const beforeSelect = this.querySelector('#before-image-select');
const afterSelect = this.querySelector('#after-image-select');
if (!beforeSelect || !afterSelect) return;
const beforeId = parseInt(beforeSelect.value);
const afterId = parseInt(afterSelect.value);
if (!beforeId || !afterId) {
ComponentHelpers.showToast?.('Please select both before and after images', 'error');
return;
}
if (beforeId === afterId) {
ComponentHelpers.showToast?.('Please select different images to compare', 'error');
return;
}
this.isComparing = true;
const compareBtn = this.querySelector('#visual-diff-compare-btn');
if (compareBtn) {
compareBtn.disabled = true;
compareBtn.textContent = '🔍 Comparing...';
}
try {
// Get screenshot objects
this.beforeImage = this.screenshots.find(s => s.id === beforeId);
this.afterImage = this.screenshots.find(s => s.id === afterId);
if (!this.beforeImage || !this.afterImage) {
throw new Error('Screenshots not found');
}
// Load image data
const beforeData = await this.loadImageData(this.beforeImage.imageData);
const afterData = await this.loadImageData(this.afterImage.imageData);
// Ensure images are same size
if (beforeData.width !== afterData.width || beforeData.height !== afterData.height) {
throw new Error(`Image dimensions don't match: ${beforeData.width}x${beforeData.height} vs ${afterData.width}x${afterData.height}`);
}
// Create diff canvas
const diffCanvas = document.createElement('canvas');
diffCanvas.width = beforeData.width;
diffCanvas.height = beforeData.height;
const diffCtx = diffCanvas.getContext('2d');
const diffImageData = diffCtx.createImageData(beforeData.width, beforeData.height);
// Run pixelmatch comparison
const numDiffPixels = pixelmatch(
beforeData.data,
afterData.data,
diffImageData.data,
beforeData.width,
beforeData.height,
{
threshold: 0.1,
includeAA: false,
alpha: 0.1,
diffColor: [255, 0, 0]
}
);
// Put diff data on canvas
diffCtx.putImageData(diffImageData, 0, 0);
// Calculate difference percentage
const totalPixels = beforeData.width * beforeData.height;
const diffPercentage = ((numDiffPixels / totalPixels) * 100).toFixed(2);
this.diffResult = {
beforeImage: this.beforeImage,
afterImage: this.afterImage,
diffImageData: diffCanvas.toDataURL(),
numDiffPixels,
totalPixels,
diffPercentage,
timestamp: new Date()
};
this.renderDiffResult();
ComponentHelpers.showToast?.(
`Comparison complete: ${diffPercentage}% difference`,
diffPercentage < 1 ? 'success' : diffPercentage < 10 ? 'warning' : 'error'
);
} catch (error) {
console.error('Failed to compare images:', error);
ComponentHelpers.showToast?.(`Comparison failed: ${error.message}`, 'error');
const diffContent = this.querySelector('#diff-result-content');
if (diffContent) {
diffContent.innerHTML = ComponentHelpers.renderError('Comparison failed', error);
}
} finally {
this.isComparing = false;
if (compareBtn) {
compareBtn.disabled = false;
compareBtn.textContent = '🔍 Compare';
}
}
}
renderSelectors() {
const beforeSelect = this.querySelector('#before-image-select');
const afterSelect = this.querySelector('#after-image-select');
if (!beforeSelect || !afterSelect) return;
const options = this.screenshots.map(screenshot => {
const timestamp = ComponentHelpers.formatTimestamp(new Date(screenshot.timestamp));
return `<option value="${screenshot.id}">${ComponentHelpers.escapeHtml(screenshot.selector)} - ${timestamp}</option>`;
}).join('');
const emptyOption = '<option value="">-- Select Screenshot --</option>';
beforeSelect.innerHTML = emptyOption + options;
afterSelect.innerHTML = emptyOption + options;
}
renderDiffResult() {
const diffContent = this.querySelector('#diff-result-content');
if (!diffContent || !this.diffResult) return;
const { diffPercentage, numDiffPixels, totalPixels } = this.diffResult;
// Determine status badge
let statusBadge;
if (diffPercentage < 1) {
statusBadge = ComponentHelpers.createBadge('Identical', 'success');
} else if (diffPercentage < 10) {
statusBadge = ComponentHelpers.createBadge('Minor Changes', 'warning');
} else {
statusBadge = ComponentHelpers.createBadge('Significant Changes', 'error');
}
diffContent.innerHTML = `
<!-- Stats Card -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<h4 style="font-size: 12px; font-weight: 600;">Comparison Result</h4>
${statusBadge}
</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; font-size: 11px;">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-text);">${diffPercentage}%</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Difference</div>
</div>
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-text);">${numDiffPixels.toLocaleString()}</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Pixels Changed</div>
</div>
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-text);">${totalPixels.toLocaleString()}</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Total Pixels</div>
</div>
</div>
</div>
<!-- Image Comparison Grid -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px;">
<!-- Before Image -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; overflow: hidden;">
<div style="padding: 12px; border-bottom: 1px solid var(--vscode-border);">
<div style="font-size: 11px; font-weight: 600; margin-bottom: 4px;">Before</div>
<div style="font-size: 10px; color: var(--vscode-text-dim);">${ComponentHelpers.escapeHtml(this.diffResult.beforeImage.selector)}</div>
<div style="font-size: 10px; color: var(--vscode-text-dim);">${ComponentHelpers.formatRelativeTime(new Date(this.diffResult.beforeImage.timestamp))}</div>
</div>
<div style="aspect-ratio: 16/9; overflow: hidden; background: var(--vscode-bg);">
<img src="${this.diffResult.beforeImage.imageData}" style="width: 100%; height: 100%; object-fit: contain;" alt="Before" />
</div>
</div>
<!-- After Image -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; overflow: hidden;">
<div style="padding: 12px; border-bottom: 1px solid var(--vscode-border);">
<div style="font-size: 11px; font-weight: 600; margin-bottom: 4px;">After</div>
<div style="font-size: 10px; color: var(--vscode-text-dim);">${ComponentHelpers.escapeHtml(this.diffResult.afterImage.selector)}</div>
<div style="font-size: 10px; color: var(--vscode-text-dim);">${ComponentHelpers.formatRelativeTime(new Date(this.diffResult.afterImage.timestamp))}</div>
</div>
<div style="aspect-ratio: 16/9; overflow: hidden; background: var(--vscode-bg);">
<img src="${this.diffResult.afterImage.imageData}" style="width: 100%; height: 100%; object-fit: contain;" alt="After" />
</div>
</div>
<!-- Diff Image -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; overflow: hidden; grid-column: span 2;">
<div style="padding: 12px; border-bottom: 1px solid var(--vscode-border);">
<div style="font-size: 11px; font-weight: 600; margin-bottom: 4px;">Visual Diff</div>
<div style="font-size: 10px; color: var(--vscode-text-dim);">Red pixels indicate changes</div>
</div>
<div style="aspect-ratio: 16/9; overflow: hidden; background: var(--vscode-bg);">
<img src="${this.diffResult.diffImageData}" style="width: 100%; height: 100%; object-fit: contain;" alt="Diff" />
</div>
</div>
</div>
<div style="margin-top: 12px; padding: 8px; background-color: var(--vscode-sidebar); border-radius: 4px; font-size: 10px; color: var(--vscode-text-dim);">
💡 Red pixels show where the images differ. Lower percentages indicate more similarity.
</div>
`;
}
render() {
this.innerHTML = `
<div style="padding: 16px; height: 100%; display: flex; flex-direction: column;">
<!-- Selector Controls -->
<div style="margin-bottom: 16px; background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px;">
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 12px;">Visual Diff Comparison</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr auto; gap: 12px; align-items: end;">
<div>
<label style="display: block; font-size: 11px; margin-bottom: 4px; color: var(--vscode-text-dim);">
Before Image
</label>
<select id="before-image-select" class="input" style="width: 100%; font-size: 11px;">
<option value="">-- Select Screenshot --</option>
</select>
</div>
<div>
<label style="display: block; font-size: 11px; margin-bottom: 4px; color: var(--vscode-text-dim);">
After Image
</label>
<select id="after-image-select" class="input" style="width: 100%; font-size: 11px;">
<option value="">-- Select Screenshot --</option>
</select>
</div>
<button id="visual-diff-compare-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
🔍 Compare
</button>
</div>
${this.screenshots.length === 0 ? `
<div style="margin-top: 12px; padding: 12px; background-color: rgba(255, 191, 0, 0.1); border-radius: 4px;">
<div style="font-size: 11px; color: #ffbf00;">
⚠️ No screenshots available. Capture screenshots using the Screenshot Gallery tool first.
</div>
</div>
` : ''}
</div>
<!-- Diff Result -->
<div id="diff-result-content" style="flex: 1; overflow-y: auto;">
<div style="text-align: center; padding: 48px; color: var(--vscode-text-dim);">
<div style="font-size: 48px; margin-bottom: 16px;">🔍</div>
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">Ready to Compare</h3>
<p style="font-size: 12px;">
Select two screenshots above and click "Compare" to see the visual differences.
</p>
</div>
</div>
</div>
`;
}
}
customElements.define('ds-visual-diff', DSVisualDiff);
export default DSVisualDiff;