/** * Design System Server (DSS) - Figma Service * * Client-side interface to Figma API tools. * No mocks - requires backend connection. */ import notificationService from './notification-service.js'; class FigmaService { constructor() { this.apiBase = '/api/figma'; this.connected = null; // null = unknown, true/false = checked this.listeners = new Set(); } // === Connection === async checkConnection() { try { const response = await fetch('/health', { method: 'GET' }); this.connected = response.ok; } catch { this.connected = false; } return this.connected; } _requireConnection() { if (this.connected === false) { throw new Error('API unavailable. Start DSS server first.'); } } // === Event System === on(event, callback) { this.listeners.add({ event, callback }); return () => this.listeners.delete({ event, callback }); } emit(event, data) { this.listeners.forEach(l => { if (l.event === event) l.callback(data); }); } // === API Methods === async extractVariables(fileKey, format = 'css') { this._requireConnection(); this.emit('loading', { operation: 'extractVariables' }); try { const response = await fetch(`${this.apiBase}/extract-variables`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_key: fileKey, format }) }); if (!response.ok) { const err = await response.json().catch(() => ({})); throw new Error(err.detail || 'Failed to extract variables'); } const result = await response.json(); this.emit('complete', { operation: 'extractVariables', result }); notificationService.create({ title: 'Variables Extracted', message: `Successfully extracted ${result.variable_count || 'variables'} from Figma.`, type: 'success', source: 'Figma', actions: [ { label: 'View Tokens', event: 'navigate', payload: { page: 'tokens' } } ] }); return result; } catch (error) { notificationService.create({ title: 'Extraction Failed', message: error.message, type: 'error', source: 'Figma' }); throw error; } } async extractComponents(fileKey) { this._requireConnection(); this.emit('loading', { operation: 'extractComponents' }); const response = await fetch(`${this.apiBase}/extract-components`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_key: fileKey }) }); if (!response.ok) { const err = await response.json().catch(() => ({})); throw new Error(err.detail || 'Failed to extract components'); } const result = await response.json(); this.emit('complete', { operation: 'extractComponents', result }); return result; } async extractStyles(fileKey) { this._requireConnection(); this.emit('loading', { operation: 'extractStyles' }); const response = await fetch(`${this.apiBase}/extract-styles`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_key: fileKey }) }); if (!response.ok) { const err = await response.json().catch(() => ({})); throw new Error(err.detail || 'Failed to extract styles'); } return response.json(); } async syncTokens(fileKey, targetPath, format = 'css') { this._requireConnection(); this.emit('loading', { operation: 'syncTokens' }); try { const response = await fetch(`${this.apiBase}/sync-tokens`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_key: fileKey, target_path: targetPath, format }) }); if (!response.ok) { const err = await response.json().catch(() => ({})); throw new Error(err.detail || 'Sync failed'); } const result = await response.json(); this.emit('complete', { operation: 'syncTokens', result }); notificationService.create({ title: 'Tokens Synced', message: `Tokens successfully written to ${result.path || targetPath}.`, type: 'success', source: 'Figma', actions: [ { label: 'View Tokens', event: 'navigate', payload: { page: 'tokens' } } ] }); return result; } catch (error) { notificationService.create({ title: 'Token Sync Failed', message: error.message, type: 'error', source: 'Figma', actions: [ { label: 'Retry', event: 'figma:sync', payload: { fileKey } } ] }); throw error; } } async visualDiff(fileKey, baselineVersion = 'latest') { this._requireConnection(); this.emit('loading', { operation: 'visualDiff' }); const response = await fetch(`${this.apiBase}/visual-diff`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_key: fileKey, baseline_version: baselineVersion }) }); if (!response.ok) { const err = await response.json().catch(() => ({})); throw new Error(err.detail || 'Visual diff failed'); } return response.json(); } async validateComponents(fileKey) { this._requireConnection(); this.emit('loading', { operation: 'validateComponents' }); const response = await fetch(`${this.apiBase}/validate-components`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_key: fileKey }) }); if (!response.ok) { const err = await response.json().catch(() => ({})); throw new Error(err.detail || 'Validation failed'); } return response.json(); } async generateCode(fileKey, componentName, framework = 'webcomponent') { this._requireConnection(); this.emit('loading', { operation: 'generateCode' }); const response = await fetch(`${this.apiBase}/generate-code`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_key: fileKey, component_name: componentName, framework }) }); if (!response.ok) { const err = await response.json().catch(() => ({})); throw new Error(err.detail || 'Code generation failed'); } return response.json(); } } const figmaService = new FigmaService(); export { FigmaService }; export default figmaService;