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
412 lines
15 KiB
JavaScript
412 lines
15 KiB
JavaScript
/**
|
|
* ds-figma-status.js
|
|
* Figma integration status and sync controls
|
|
*/
|
|
|
|
import toolBridge from '../../services/tool-bridge.js';
|
|
import { ComponentHelpers } from '../../utils/component-helpers.js';
|
|
|
|
class DSFigmaStatus extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.figmaToken = null;
|
|
this.figmaFileKey = null;
|
|
this.connectionStatus = 'unknown';
|
|
this.lastSync = null;
|
|
this.isConfiguring = false;
|
|
this.isSyncing = false;
|
|
}
|
|
|
|
async connectedCallback() {
|
|
this.render();
|
|
this.setupEventListeners();
|
|
await this.checkConfiguration();
|
|
}
|
|
|
|
/**
|
|
* Check if Figma is configured and test connection
|
|
*/
|
|
async checkConfiguration() {
|
|
const statusContent = this.querySelector('#figma-status-content');
|
|
if (!statusContent) return;
|
|
|
|
try {
|
|
// Check for stored file key in localStorage (not token - that's server-side)
|
|
this.figmaFileKey = localStorage.getItem('figma_file_key');
|
|
|
|
if (!this.figmaFileKey) {
|
|
this.connectionStatus = 'not_configured';
|
|
this.renderStatus();
|
|
return;
|
|
}
|
|
|
|
// Test connection by calling sync with dry-run check
|
|
// Note: Backend checks for FIGMA_TOKEN env variable
|
|
statusContent.innerHTML = ComponentHelpers.renderLoading('Checking Figma connection...');
|
|
|
|
try {
|
|
// Try to get Figma file info (will fail if token not configured)
|
|
const result = await toolBridge.syncFigma(this.figmaFileKey);
|
|
|
|
if (result && result.tokens) {
|
|
this.connectionStatus = 'connected';
|
|
this.lastSync = new Date();
|
|
} else {
|
|
this.connectionStatus = 'error';
|
|
}
|
|
} catch (error) {
|
|
// Token not configured on backend
|
|
if (error.message.includes('FIGMA_TOKEN')) {
|
|
this.connectionStatus = 'token_missing';
|
|
} else {
|
|
this.connectionStatus = 'error';
|
|
}
|
|
console.error('Figma connection check failed:', error);
|
|
}
|
|
|
|
this.renderStatus();
|
|
} catch (error) {
|
|
console.error('Failed to check Figma configuration:', error);
|
|
statusContent.innerHTML = ComponentHelpers.renderError('Failed to check configuration', error);
|
|
}
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Configure button
|
|
const configureBtn = this.querySelector('#figma-configure-btn');
|
|
if (configureBtn) {
|
|
configureBtn.addEventListener('click', () => this.showConfiguration());
|
|
}
|
|
|
|
// Sync button
|
|
const syncBtn = this.querySelector('#figma-sync-btn');
|
|
if (syncBtn) {
|
|
syncBtn.addEventListener('click', () => this.syncFromFigma());
|
|
}
|
|
}
|
|
|
|
showConfiguration() {
|
|
this.isConfiguring = true;
|
|
this.renderStatus();
|
|
|
|
// Setup save handler
|
|
const saveBtn = this.querySelector('#figma-save-config-btn');
|
|
const cancelBtn = this.querySelector('#figma-cancel-config-btn');
|
|
|
|
if (saveBtn) {
|
|
saveBtn.addEventListener('click', () => this.saveConfiguration());
|
|
}
|
|
|
|
if (cancelBtn) {
|
|
cancelBtn.addEventListener('click', () => {
|
|
this.isConfiguring = false;
|
|
this.renderStatus();
|
|
});
|
|
}
|
|
}
|
|
|
|
async saveConfiguration() {
|
|
const fileKeyInput = this.querySelector('#figma-file-key-input');
|
|
const tokenInput = this.querySelector('#figma-token-input');
|
|
|
|
if (!fileKeyInput || !tokenInput) return;
|
|
|
|
const fileKey = fileKeyInput.value.trim();
|
|
const token = tokenInput.value.trim();
|
|
|
|
if (!fileKey) {
|
|
ComponentHelpers.showToast?.('Please enter a Figma file key', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!token) {
|
|
ComponentHelpers.showToast?.('Please enter a Figma access token', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Store file key in localStorage (client-side)
|
|
localStorage.setItem('figma_file_key', fileKey);
|
|
this.figmaFileKey = fileKey;
|
|
|
|
// Display warning about backend token configuration
|
|
ComponentHelpers.showToast?.('File key saved. Please configure FIGMA_TOKEN environment variable on the backend.', 'info');
|
|
|
|
this.isConfiguring = false;
|
|
this.connectionStatus = 'token_missing';
|
|
this.renderStatus();
|
|
} catch (error) {
|
|
console.error('Failed to save Figma configuration:', error);
|
|
ComponentHelpers.showToast?.(`Failed to save configuration: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
async syncFromFigma() {
|
|
if (this.isSyncing || !this.figmaFileKey) return;
|
|
|
|
this.isSyncing = true;
|
|
const syncBtn = this.querySelector('#figma-sync-btn');
|
|
|
|
if (syncBtn) {
|
|
syncBtn.disabled = true;
|
|
syncBtn.textContent = '🔄 Syncing...';
|
|
}
|
|
|
|
try {
|
|
const result = await toolBridge.syncFigma(this.figmaFileKey);
|
|
|
|
if (result && result.tokens) {
|
|
this.lastSync = new Date();
|
|
this.connectionStatus = 'connected';
|
|
|
|
ComponentHelpers.showToast?.(
|
|
`Synced ${Object.keys(result.tokens).length} tokens from Figma`,
|
|
'success'
|
|
);
|
|
|
|
this.renderStatus();
|
|
} else {
|
|
throw new Error('No tokens returned from Figma');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to sync from Figma:', error);
|
|
ComponentHelpers.showToast?.(`Sync failed: ${error.message}`, 'error');
|
|
this.connectionStatus = 'error';
|
|
this.renderStatus();
|
|
} finally {
|
|
this.isSyncing = false;
|
|
if (syncBtn) {
|
|
syncBtn.disabled = false;
|
|
syncBtn.textContent = '🔄 Sync Now';
|
|
}
|
|
}
|
|
}
|
|
|
|
getStatusBadge() {
|
|
const badges = {
|
|
connected: ComponentHelpers.createBadge('Connected', 'success'),
|
|
not_configured: ComponentHelpers.createBadge('Not Configured', 'info'),
|
|
token_missing: ComponentHelpers.createBadge('Token Required', 'warning'),
|
|
error: ComponentHelpers.createBadge('Error', 'error'),
|
|
unknown: ComponentHelpers.createBadge('Unknown', 'info')
|
|
};
|
|
|
|
return badges[this.connectionStatus] || badges.unknown;
|
|
}
|
|
|
|
renderStatus() {
|
|
const statusContent = this.querySelector('#figma-status-content');
|
|
if (!statusContent) return;
|
|
|
|
// Configuration form
|
|
if (this.isConfiguring) {
|
|
statusContent.innerHTML = `
|
|
<div style="padding: 16px; background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px;">
|
|
<h4 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Configure Figma Integration</h4>
|
|
|
|
<div style="margin-bottom: 12px;">
|
|
<label style="display: block; font-size: 11px; margin-bottom: 4px; color: var(--vscode-text-dim);">
|
|
Figma File Key
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="figma-file-key-input"
|
|
class="input"
|
|
placeholder="e.g., abc123xyz456"
|
|
value="${ComponentHelpers.escapeHtml(this.figmaFileKey || '')}"
|
|
style="width: 100%; font-size: 11px; font-family: 'Courier New', monospace;"
|
|
/>
|
|
<div style="font-size: 10px; color: var(--vscode-text-dim); margin-top: 4px;">
|
|
Find this in your Figma file URL: figma.com/file/<strong>FILE_KEY</strong>/...
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 16px;">
|
|
<label style="display: block; font-size: 11px; margin-bottom: 4px; color: var(--vscode-text-dim);">
|
|
Figma Access Token
|
|
</label>
|
|
<input
|
|
type="password"
|
|
id="figma-token-input"
|
|
class="input"
|
|
placeholder="figd_..."
|
|
style="width: 100%; font-size: 11px; font-family: 'Courier New', monospace;"
|
|
/>
|
|
<div style="font-size: 10px; color: var(--vscode-text-dim); margin-top: 4px;">
|
|
Generate at: <a href="https://www.figma.com/developers/api#access-tokens" target="_blank" style="color: var(--vscode-accent);">figma.com/developers/api</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="padding: 12px; background-color: rgba(255, 191, 0, 0.1); border-radius: 4px; margin-bottom: 16px;">
|
|
<div style="font-size: 11px; color: #ffbf00;">
|
|
⚠️ <strong>Security Note:</strong> The Figma token must be configured as the <code>FIGMA_TOKEN</code> environment variable on the backend server. This UI only stores the file key locally.
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 8px; justify-content: flex-end;">
|
|
<button id="figma-cancel-config-btn" class="button" style="padding: 6px 12px; font-size: 11px;">
|
|
Cancel
|
|
</button>
|
|
<button id="figma-save-config-btn" class="button" style="padding: 6px 12px; font-size: 11px;">
|
|
Save Configuration
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Not configured state
|
|
if (this.connectionStatus === 'not_configured') {
|
|
statusContent.innerHTML = `
|
|
<div style="text-align: center; padding: 32px;">
|
|
<div style="font-size: 48px; margin-bottom: 16px;">🎨</div>
|
|
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">Figma Not Configured</h3>
|
|
<p style="font-size: 12px; color: var(--vscode-text-dim); margin-bottom: 16px;">
|
|
Connect your Figma file to sync design tokens automatically.
|
|
</p>
|
|
<button id="figma-configure-btn" class="button" style="padding: 8px 16px; font-size: 12px;">
|
|
Configure Figma
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
const configureBtn = statusContent.querySelector('#figma-configure-btn');
|
|
if (configureBtn) {
|
|
configureBtn.addEventListener('click', () => this.showConfiguration());
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Token missing state
|
|
if (this.connectionStatus === 'token_missing') {
|
|
statusContent.innerHTML = `
|
|
<div style="padding: 16px; background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
|
<h4 style="font-size: 12px; font-weight: 600;">Figma Configuration</h4>
|
|
${this.getStatusBadge()}
|
|
</div>
|
|
|
|
<div style="padding: 12px; background-color: rgba(255, 191, 0, 0.1); border: 1px solid #ffbf00; border-radius: 4px; margin-bottom: 12px;">
|
|
<div style="font-size: 11px; color: #ffbf00;">
|
|
⚠️ <strong>Backend Configuration Required</strong><br/>
|
|
Please set the <code>FIGMA_TOKEN</code> environment variable on the backend server and restart.
|
|
</div>
|
|
</div>
|
|
|
|
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 8px;">
|
|
<strong>File Key:</strong> <code style="background-color: var(--vscode-bg); padding: 2px 6px; border-radius: 2px;">${ComponentHelpers.escapeHtml(this.figmaFileKey || 'N/A')}</code>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 8px; margin-top: 12px;">
|
|
<button id="figma-configure-btn" class="button" style="padding: 4px 12px; font-size: 11px; flex: 1;">
|
|
Reconfigure
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const configureBtn = statusContent.querySelector('#figma-configure-btn');
|
|
if (configureBtn) {
|
|
configureBtn.addEventListener('click', () => this.showConfiguration());
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Connected state
|
|
if (this.connectionStatus === 'connected') {
|
|
statusContent.innerHTML = `
|
|
<div style="padding: 16px; background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
|
<h4 style="font-size: 12px; font-weight: 600;">Figma Sync</h4>
|
|
${this.getStatusBadge()}
|
|
</div>
|
|
|
|
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 8px;">
|
|
<strong>File Key:</strong> <code style="background-color: var(--vscode-bg); padding: 2px 6px; border-radius: 2px;">${ComponentHelpers.escapeHtml(this.figmaFileKey || 'N/A')}</code>
|
|
</div>
|
|
|
|
${this.lastSync ? `
|
|
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 12px;">
|
|
<strong>Last Sync:</strong> ${ComponentHelpers.formatRelativeTime(this.lastSync)}
|
|
</div>
|
|
` : ''}
|
|
|
|
<div style="display: flex; gap: 8px; margin-top: 12px;">
|
|
<button id="figma-sync-btn" class="button" style="padding: 4px 12px; font-size: 11px; flex: 1;">
|
|
🔄 Sync Now
|
|
</button>
|
|
<button id="figma-configure-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
|
|
⚙️
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const syncBtn = statusContent.querySelector('#figma-sync-btn');
|
|
const configureBtn = statusContent.querySelector('#figma-configure-btn');
|
|
|
|
if (syncBtn) {
|
|
syncBtn.addEventListener('click', () => this.syncFromFigma());
|
|
}
|
|
|
|
if (configureBtn) {
|
|
configureBtn.addEventListener('click', () => this.showConfiguration());
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Error state
|
|
if (this.connectionStatus === 'error') {
|
|
statusContent.innerHTML = `
|
|
<div style="padding: 16px; background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
|
<h4 style="font-size: 12px; font-weight: 600;">Figma Sync</h4>
|
|
${this.getStatusBadge()}
|
|
</div>
|
|
|
|
<div style="padding: 12px; background-color: rgba(244, 135, 113, 0.1); border: 1px solid #f48771; border-radius: 4px; margin-bottom: 12px;">
|
|
<div style="font-size: 11px; color: #f48771;">
|
|
❌ Failed to connect to Figma. Please check your configuration.
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 8px;">
|
|
<button id="figma-configure-btn" class="button" style="padding: 4px 12px; font-size: 11px; flex: 1;">
|
|
Reconfigure
|
|
</button>
|
|
<button id="figma-sync-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
|
|
Retry
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const configureBtn = statusContent.querySelector('#figma-configure-btn');
|
|
const syncBtn = statusContent.querySelector('#figma-sync-btn');
|
|
|
|
if (configureBtn) {
|
|
configureBtn.addEventListener('click', () => this.showConfiguration());
|
|
}
|
|
|
|
if (syncBtn) {
|
|
syncBtn.addEventListener('click', () => this.checkConfiguration());
|
|
}
|
|
}
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<div style="padding: 16px; height: 100%; display: flex; flex-direction: column;">
|
|
<div id="figma-status-content" style="flex: 1;">
|
|
${ComponentHelpers.renderLoading('Checking Figma configuration...')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define('ds-figma-status', DSFigmaStatus);
|
|
|
|
export default DSFigmaStatus;
|