Files
dss/admin-ui/js/core/app.js
Digital Production Factory 276ed71f31 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
2025-12-09 18:45:48 -03:00

4351 lines
155 KiB
JavaScript

/**
* Design System Server (DSS) - App Shell
*
* Main application controller.
* Handles routing, state, and integration with services.
*/
import store from '../stores/app-store.js';
import figmaService from '../services/figma-service.js';
import discoveryService from '../services/discovery-service.js';
import teamService from '../services/team-service.js';
import toolsService from '../services/tools-service.js';
import claudeService from '../services/claude-service.js';
import DashboardService from '../services/dashboard-service.js';
import pluginService from '../services/plugin-service.js';
import { initializePlugins } from '../plugins/index.js';
import logger from './logger.js';
import themeManager from './theme.js';
import { UITeamDashboard, UXTeamDashboard, QATeamDashboard } from './team-dashboards.js';
// Enterprise Architecture Modules
import router from './router.js';
import { subscribe as subscribeToNotifications, notifySuccess, notifyError, notifyInfo, ErrorCode } from './messaging.js';
import { createProjectWorkflow } from './workflows.js';
import { handleError } from './error-handler.js';
import { loadConfig, getStorybookUrl, getDssHost } from './config-loader.js';
import { getEnabledComponents, getComponentSetting, setComponentSetting } from './component-config.js';
import { sanitizeHtml, setSafeHtml, sanitizeText, escapeHtml } from './sanitizer.js';
import LandingPage from './landing-page.js';
import themeLoader from './theme-loader.js';
class App {
constructor() {
this.store = store;
this.services = {
figma: figmaService,
discovery: discoveryService,
team: teamService
};
// Track listener setup to prevent duplicates
this.listeners = {
teamContextChanged: null,
hasTeamContextListener: false
};
}
// === Initialization ===
async init() {
logger.info('App', 'Initializing application...');
// FIRST: Load server configuration (blocking operation)
// This must complete before any component uses the config
try {
await loadConfig();
this.store.set({ isConfigLoading: false });
logger.info('App', 'Server configuration loaded');
} catch (error) {
this.store.set({
isConfigLoading: false,
configError: error.message
});
logger.error('App', 'Failed to load server configuration', { error: error.message });
notifyError('Failed to load server configuration. Some features may not work correctly.', ErrorCode.SYSTEM_STARTUP_FAILED);
}
// Initialize DSS Theme Loader (validates CSS layers)
try {
await themeLoader.init();
this.themeLoader = themeLoader;
logger.info('App', 'DSS Theme Loader initialized');
} catch (error) {
logger.warn('App', 'Theme loader initialization failed, using fallback styles', { error: error.message });
}
// Initialize plugins
initializePlugins();
logger.info('App', 'Plugins initialized');
// Initialize enterprise messaging system
this.initializeMessaging();
// Initialize router with route definitions
this.initializeRouter();
// Subscribe to state changes
this.store.subscribe('currentPage', () => this.render());
this.store.subscribe('notifications', (notifs) => this.renderNotifications(notifs));
this.store.subscribe('currentProject', (projectId) => {
if (projectId) {
this.loadDashboardData(projectId);
}
});
// Listen for team context changes from chat sidebar (only add listener once)
if (!this.listeners.hasTeamContextListener) {
this.listeners.teamContextChanged = (e) => {
logger.info('App', 'Team context changed', { team: e.detail.team });
this.store.set({ teamContext: e.detail.team });
this.render();
};
window.addEventListener('team-context-changed', this.listeners.teamContextChanged);
this.listeners.hasTeamContextListener = true;
}
// Load initial data
await this.loadInitialData();
// Update Storybook link with configured URL
this.updateStorybookLink();
// Initialize landing page with dashboard cards
const appElement = document.getElementById('app');
if (appElement) {
this.landingPage = new LandingPage(appElement);
logger.info('App', 'Landing page initialized');
}
// Render the app
this.render();
// Set up polling
setInterval(() => this.refreshHealth(), 30000);
logger.info('App', 'Application initialized successfully');
}
/**
* Initialize enterprise messaging system
*/
initializeMessaging() {
// Subscribe to notification events
subscribeToNotifications((notification) => {
// Use the existing store notification system for UI display
const duration = notification.duration || 5000;
this.store.notify(notification.message, notification.type, duration);
// Log to console for debugging
logger.info('Notification', notification.message, {
code: notification.code,
correlationId: notification.correlationId,
});
});
logger.info('App', 'Messaging system initialized');
}
/**
* Initialize router with route definitions
*/
initializeRouter() {
// Register all application routes
router.registerAll([
{
path: 'dashboard',
name: 'dashboard',
handler: ({ router }) => {
this.store.set({ currentPage: 'dashboard' });
this.render();
},
},
{
path: 'projects',
name: 'projects',
handler: ({ router }) => {
this.store.set({ currentPage: 'projects' });
this.render();
},
},
{
path: 'tokens',
name: 'tokens',
handler: ({ router }) => {
this.store.set({ currentPage: 'tokens' });
this.render();
},
},
{
path: 'components',
name: 'components',
handler: ({ router }) => {
this.store.set({ currentPage: 'components' });
this.render();
},
},
{
path: 'figma',
name: 'figma',
handler: ({ router }) => {
this.store.set({ currentPage: 'figma' });
this.render();
},
},
{
path: 'docs',
name: 'docs',
handler: ({ router }) => {
this.store.set({ currentPage: 'docs' });
this.render();
},
},
{
path: 'teams',
name: 'teams',
handler: ({ router }) => {
this.store.set({ currentPage: 'teams' });
this.render();
},
},
{
path: 'audit',
name: 'audit',
handler: ({ router }) => {
this.store.set({ currentPage: 'audit' });
this.render();
},
},
{
path: 'settings',
name: 'settings',
handler: ({ router }) => {
this.store.set({ currentPage: 'settings' });
this.render();
},
},
{
path: 'services',
name: 'services',
handler: ({ router }) => {
this.store.set({ currentPage: 'services' });
this.render();
},
},
{
path: 'quick-wins',
name: 'quick-wins',
handler: ({ router }) => {
this.store.set({ currentPage: 'quick-wins' });
this.render();
},
},
{
path: 'chat',
name: 'chat',
handler: ({ router }) => {
this.store.set({ currentPage: 'chat' });
this.render();
},
},
{
path: 'plugins',
name: 'plugins',
handler: ({ router }) => {
this.store.set({ currentPage: 'plugins' });
this.render();
},
},
{
path: 'workdesk',
name: 'workdesk',
handler: async ({ router }) => {
logger.info('App', 'Mounting workdesk UI');
// Get app container
const appRoot = document.getElementById('app');
if (!appRoot) {
logger.error('App', 'Cannot mount workdesk: #app element not found');
return;
}
// Full page takeover - clear legacy layout
appRoot.innerHTML = '';
// Dynamically import and mount ds-shell
try {
await import('../components/layout/ds-shell.js');
const shell = document.createElement('ds-shell');
shell.setAttribute('mode', 'production');
appRoot.appendChild(shell);
logger.info('App', 'Workdesk mounted successfully');
} catch (error) {
logger.error('App', 'Failed to mount workdesk', { error: error.message });
appRoot.innerHTML = `
<div style="padding: 48px; text-align: center; color: #f48771;">
<h2>Failed to Load Workdesk</h2>
<p style="margin-top: 16px;">Error: ${error.message}</p>
<button onclick="window.location.hash='dashboard'" style="margin-top: 24px; padding: 8px 16px;">
Return to Dashboard
</button>
</div>
`;
}
},
},
]);
// Set default route
router.setDefaultRoute('dashboard');
// Initialize router
router.init();
logger.info('App', 'Router initialized with 13 routes');
}
async loadInitialData() {
this.store.setLoading('init', true);
try {
// Check API connection first
const health = await this.services.discovery.getHealth();
this.store.setHealth(health);
this.store.set({ apiConnected: true });
// Load runtime configuration and services
await this.loadConfig();
// Load discovery data
const discovery = await this.services.discovery.discover('.', false);
this.store.setDiscovery(discovery);
// Load stats
const stats = await this.services.discovery.getProjectStats();
this.store.setStats(stats);
// Load recent activity
const activity = await this.services.discovery.getRecentActivity();
this.store.setActivity(activity.items || []);
// Load projects
await this.loadProjects();
// Set user from session or default
this.store.setUser(
{ id: '1', name: 'User', email: 'user@local' },
'team-1',
'TEAM_LEAD'
);
} catch (error) {
logger.error('App', 'Failed to load initial data', error);
this.store.set({ apiConnected: false });
this.store.setError('init', 'Cannot connect to DSS server. Run: python tools/api/server.py');
this.store.notify('Server offline. Start DSS API first.', 'error', 0);
}
this.store.setLoading('init', false);
}
async refreshHealth() {
try {
const health = await this.services.discovery.getHealth();
this.store.setHealth(health);
} catch (error) {
logger.error('App', 'Health check failed', error);
}
}
// === Navigation ===
navigate(page) {
window.location.hash = page;
}
// === Figma Actions ===
async extractTokens() {
const fileKey = this.store.get('figmaFileKey') || 'demo';
this.store.setLoading('extractTokens', true);
try {
const result = await this.services.figma.extractVariables(fileKey, 'css');
this.store.setTokens(result.tokens);
notifySuccess('Tokens extracted successfully!', ErrorCode.SUCCESS_OPERATION, {
fileKey,
tokenCount: result.tokens?.length || 0,
});
return result;
} catch (error) {
// Use enterprise error handler for user-friendly messages
handleError(error, {
operation: 'extract tokens',
service: 'figma',
fileKey,
});
throw error;
} finally {
this.store.setLoading('extractTokens', false);
}
}
async extractComponents() {
const fileKey = this.store.get('figmaFileKey') || 'demo';
this.store.setLoading('extractComponents', true);
try {
const result = await this.services.figma.extractComponents(fileKey);
this.store.setComponents(result.components);
this.store.notify('Components extracted successfully', 'success');
return result;
} catch (error) {
this.store.notify(`Failed to extract components: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('extractComponents', false);
}
}
async syncTokens() {
const fileKey = this.store.get('figmaFileKey') || 'demo';
this.store.setLoading('syncTokens', true);
try {
const result = await this.services.figma.syncTokens(fileKey, './admin-ui/css/tokens.css');
this.store.setLastSync(new Date().toISOString());
const count = result?.tokens_synced ?? 0;
this.store.notify(count > 0 ? `Synced ${count} tokens` : 'Sync complete', 'success');
return result;
} catch (error) {
this.store.notify(`Sync failed: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('syncTokens', false);
}
}
async runVisualDiff() {
const fileKey = this.store.get('figmaFileKey') || 'demo';
this.store.setLoading('visualDiff', true);
try {
const result = await this.services.figma.visualDiff(fileKey);
if (result.changes_detected) {
this.store.notify(`Visual diff: ${result.summary.changed} changes detected`, 'warning');
} else {
this.store.notify('No visual changes detected', 'success');
}
return result;
} catch (error) {
this.store.notify(`Visual diff failed: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('visualDiff', false);
}
}
async validateComponents() {
const fileKey = this.store.get('figmaFileKey') || 'demo';
this.store.setLoading('validate', true);
try {
const result = await this.services.figma.validateComponents(fileKey);
const { errors, warnings } = result.summary;
if (errors > 0) {
this.store.notify(`Validation: ${errors} errors, ${warnings} warnings`, 'error');
} else if (warnings > 0) {
this.store.notify(`Validation passed with ${warnings} warnings`, 'warning');
} else {
this.store.notify('All components valid', 'success');
}
return result;
} catch (error) {
this.store.notify(`Validation failed: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('validate', false);
}
}
async generateCode(componentName, framework = null) {
const fileKey = this.store.get('figmaFileKey') || 'demo';
const targetFramework = framework || this.store.get('componentFramework') || 'react';
this.store.setLoading('generateCode', true);
try {
const result = await this.services.figma.generateCode(fileKey, componentName, targetFramework);
this.store.set({
generatedCode: {
component: componentName,
framework: targetFramework,
code: result.code || result.generated_code || '// Code generation in progress...'
}
});
this.store.notify(`Generated ${componentName} for ${targetFramework}`, 'success');
this.render();
return result;
} catch (error) {
this.store.notify(`Code generation failed: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('generateCode', false);
}
}
// === Discovery Actions ===
getProjectRoot() {
const project = this.store.get('selectedProject');
return project?.root_path || project?.path || '.';
}
async runDiscovery(fullScan = false) {
this.store.setLoading('discovery', true);
const projectRoot = this.getProjectRoot();
try {
const result = await this.services.discovery.discover(projectRoot, fullScan);
this.store.setDiscovery(result);
const pathInfo = projectRoot !== '.' ? ` (${projectRoot})` : '';
this.store.notify(`Discovery complete${pathInfo}`, 'success');
return result;
} catch (error) {
this.store.notify(`Discovery failed: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('discovery', false);
}
}
// === Rendering ===
render() {
const contentArea = document.getElementById('page-content');
if (!contentArea) return;
const page = this.store.get('currentPage');
setSafeHtml(contentArea, this.getPageContent(page));
// Update active nav item
document.querySelectorAll('.nav-item[data-page]').forEach(item => {
item.classList.toggle('active', item.dataset.page === page);
});
// Attach event handlers
this.attachEventHandlers();
}
getPageContent(page) {
switch (page) {
case 'dashboard': return this.renderDashboard();
case 'services': return this.renderServices();
case 'quick-wins': return this.renderQuickWins();
case 'chat': return this.renderChat();
case 'projects': return this.renderProjects();
case 'tokens': return this.renderTokens();
case 'components': return this.renderComponents();
case 'figma': return this.renderFigma();
case 'docs': return this.renderDocs();
case 'teams': return this.renderTeams();
case 'plugins': return this.renderPlugins();
case 'audit': return this.renderAuditLog();
case 'settings': return this.renderSettings();
default: return this.renderDashboard();
}
}
renderDashboard() {
const teamContext = this.store.get('teamContext') || localStorage.getItem('dss_team_context') || 'all';
const health = this.store.get('health') || {};
const stats = this.store.get('stats') || {};
const discovery = this.store.get('discovery') || {};
const activity = this.store.get('activity') || [];
const dashboardData = this.store.get('dashboardData') || {
ux: { figma_files_count: 0, figma_files: [] },
ui: { token_drift: { total: 0, by_severity: {} }, code_metrics: {} },
qa: { esre_count: 0, test_summary: {} }
};
const healthScore = discovery.health?.score || 0;
const healthGrade = discovery.health?.grade || '-';
const teamNames = {
'all': 'All Teams',
'ui': 'UI Team',
'ux': 'UX Team',
'qa': 'QA Team'
};
// Get current project
const currentProjectId = this.store.get('currentProject');
const projects = this.store.get('projects') || [];
const currentProject = projects.find(p => p.id === currentProjectId);
const projectName = currentProject?.name || 'No Project Selected';
// Render team-specific dashboard
switch(teamContext) {
case 'ui':
return UITeamDashboard(health, stats, discovery, activity, projectName, dashboardData, currentProjectId, this);
case 'ux':
return UXTeamDashboard(health, stats, discovery, activity, projectName, dashboardData, currentProjectId, this);
case 'qa':
return QATeamDashboard(health, stats, discovery, activity, projectName, dashboardData, currentProjectId, this);
default:
return this.renderAllTeamsDashboard(health, stats, discovery, activity, healthScore, healthGrade);
}
}
renderAllTeamsDashboard(health, stats, discovery, activity, healthScore, healthGrade) {
return `
<div class="page-header">
<h1>Dashboard</h1>
<p class="text-muted">Design System Orchestrator Overview · <span class="text-primary">Select a team in chat panel →</span></p>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-4 gap-4 mt-6">
<ds-card>
<ds-card-content>
<div class="stat">
<div class="stat__label">Health Score</div>
<div class="stat__value flex items-center gap-2">
<span class="status-dot ${healthScore >= 80 ? 'status-dot--success' : healthScore >= 60 ? 'status-dot--warning' : 'status-dot--error'}"></span>
${healthScore}% (${healthGrade})
</div>
</div>
</ds-card-content>
</ds-card>
<ds-card>
<ds-card-content>
<div class="stat">
<div class="stat__label">Design Tokens</div>
<div class="stat__value">${stats.tokens?.total || 0}</div>
</div>
</ds-card-content>
</ds-card>
<ds-card>
<ds-card-content>
<div class="stat">
<div class="stat__label">Components</div>
<div class="stat__value">${stats.components?.total || discovery.files?.components || 0}</div>
</div>
</ds-card-content>
</ds-card>
<ds-card>
<ds-card-content>
<div class="stat">
<div class="stat__label">Syncs Today</div>
<div class="stat__value">${stats.syncs?.today || 0}</div>
</div>
</ds-card-content>
</ds-card>
</div>
<!-- Design System Ingestion -->
<ds-card class="mt-6">
<ds-card-header>
<ds-card-title>Ingest Design System</ds-card-title>
<ds-card-description>Add a design system using natural language</ds-card-description>
</ds-card-header>
<ds-card-content>
<div class="flex gap-3">
<input
type="text"
id="ingest-prompt"
class="input flex-1"
placeholder='Try: "add heroui", "ingest material ui", "use shadcn"...'
style="font-size: 0.95rem; padding: 0.75rem 1rem;"
/>
<ds-button data-variant="primary" data-action="parseIngestion" ${this.store.isLoading('ingestion') ? 'loading' : ''}>
Ingest
</ds-button>
<ds-button data-variant="outline" data-action="browseDesignSystems">
Browse
</ds-button>
</div>
${this.renderIngestionResult()}
</ds-card-content>
</ds-card>
<!-- Main Content -->
<div class="grid grid-cols-2 gap-6 mt-6">
<!-- Quick Actions -->
<ds-card>
<ds-card-header>
<ds-card-title>Quick Actions</ds-card-title>
<ds-card-description>Common operations</ds-card-description>
</ds-card-header>
<ds-card-content>
<div class="flex flex-wrap gap-3">
<ds-button data-variant="primary" data-action="extractTokens" ${this.store.isLoading('extractTokens') ? 'loading' : ''}>
Extract Tokens
</ds-button>
<ds-button data-variant="outline" data-action="syncTokens" ${this.store.isLoading('syncTokens') ? 'loading' : ''}>
Sync Tokens
</ds-button>
<ds-button data-variant="outline" data-action="validateComponents" ${this.store.isLoading('validate') ? 'loading' : ''}>
Validate
</ds-button>
<ds-button data-variant="outline" data-action="runVisualDiff" ${this.store.isLoading('visualDiff') ? 'loading' : ''}>
Visual Diff
</ds-button>
<ds-button data-variant="ghost" data-action="runDiscovery">
Re-scan Project
</ds-button>
</div>
</ds-card-content>
</ds-card>
<!-- Project Info -->
<ds-card>
<ds-card-header>
<ds-card-title>Project Info</ds-card-title>
<ds-card-description>Discovered configuration</ds-card-description>
</ds-card-header>
<ds-card-content>
<div class="flex flex-col gap-2 text-sm">
<div class="flex justify-between">
<span class="text-muted">Project Types:</span>
<span>${(discovery.project?.types || []).join(', ') || 'Unknown'}</span>
</div>
<div class="flex justify-between">
<span class="text-muted">Frameworks:</span>
<span>${(discovery.project?.frameworks || []).join(', ') || 'None detected'}</span>
</div>
<div class="flex justify-between">
<span class="text-muted">Total Files:</span>
<span>${discovery.files?.total || 0}</span>
</div>
<div class="flex justify-between">
<span class="text-muted">CSS Files:</span>
<span>${discovery.files?.css || 0}</span>
</div>
<div class="flex justify-between">
<span class="text-muted">Git Branch:</span>
<span>${discovery.git?.branch || 'N/A'}</span>
</div>
<div class="flex justify-between">
<span class="text-muted">Uncommitted:</span>
<span>${discovery.git?.uncommitted_changes || 0} changes</span>
</div>
</div>
</ds-card-content>
</ds-card>
</div>
<!-- Recent Activity & Health Checks -->
<div class="grid grid-cols-2 gap-6 mt-6">
<ds-card>
<ds-card-header>
<ds-card-title>Recent Activity</ds-card-title>
</ds-card-header>
<ds-card-content>
${activity.length > 0 ? `
<div class="flex flex-col gap-3">
${activity.slice(0, 5).map(item => `
<div class="flex items-center gap-3 text-sm">
<span class="status-dot ${item.status === 'success' ? 'status-dot--success' : item.status === 'warning' ? 'status-dot--warning' : 'status-dot--error'}"></span>
<span class="flex-1">${item.message}</span>
<span class="text-muted text-xs">${this.formatTime(item.timestamp)}</span>
</div>
`).join('')}
</div>
` : '<p class="text-muted text-sm">No recent activity</p>'}
</ds-card-content>
</ds-card>
<ds-card>
<ds-card-header>
<ds-card-title>System Health</ds-card-title>
</ds-card-header>
<ds-card-content>
${health.checks ? `
<div class="flex flex-col gap-3">
${health.checks.map(check => `
<div class="flex items-center gap-3 text-sm">
<span class="status-dot ${check.status === 'ok' ? 'status-dot--success' : check.status === 'warning' ? 'status-dot--warning' : 'status-dot--error'}"></span>
<span class="flex-1">${check.name}</span>
<span class="text-muted text-xs">${check.message || (check.latency ? `${check.latency}ms` : 'OK')}</span>
</div>
`).join('')}
</div>
` : '<p class="text-muted text-sm">Loading health status...</p>'}
</ds-card-content>
</ds-card>
</div>
<!-- Health Issues -->
${discovery.health?.issues?.length > 0 ? `
<ds-card class="mt-6">
<ds-card-header>
<ds-card-title>Improvement Suggestions</ds-card-title>
</ds-card-header>
<ds-card-content>
<div class="flex flex-col gap-2">
${discovery.health.issues.map(issue => `
<div class="flex items-center gap-3 text-sm">
<ds-badge data-variant="warning">Suggestion</ds-badge>
<span>${issue}</span>
</div>
`).join('')}
</div>
</ds-card-content>
</ds-card>
` : ''}
`;
}
renderTokens() {
const tokens = this.store.get('tokens') || [];
const selectedProject = this.store.get('selectedProject');
const exportFormat = this.store.get('tokenExportFormat') || 'css';
const byCategory = tokens.reduce((acc, t) => {
const cat = t.category || 'other';
if (!acc[cat]) acc[cat] = [];
acc[cat].push(t);
return acc;
}, {});
const categoryOrder = ['color', 'spacing', 'sizing', 'typography', 'radius', 'shadow', 'other'];
const sortedCategories = Object.keys(byCategory).sort((a, b) => {
return categoryOrder.indexOf(a) - categoryOrder.indexOf(b);
});
const stats = {
total: tokens.length,
colors: byCategory.color?.length || 0,
spacing: byCategory.spacing?.length || 0,
typography: byCategory.typography?.length || 0
};
return `
<div class="page-header">
<h1>Design Tokens</h1>
<p class="text-muted">
${selectedProject ? `Tokens from ${selectedProject.name}` : 'Extract and manage design tokens from Figma'}
</p>
</div>
<!-- Stats Row -->
<div class="grid grid-cols-4 gap-4 mt-6">
<ds-card>
<ds-card-content>
<div class="stat">
<div class="stat__label">Total Tokens</div>
<div class="stat__value">${stats.total}</div>
</div>
</ds-card-content>
</ds-card>
<ds-card>
<ds-card-content>
<div class="stat">
<div class="stat__label">Colors</div>
<div class="stat__value">${stats.colors}</div>
</div>
</ds-card-content>
</ds-card>
<ds-card>
<ds-card-content>
<div class="stat">
<div class="stat__label">Spacing</div>
<div class="stat__value">${stats.spacing}</div>
</div>
</ds-card-content>
</ds-card>
<ds-card>
<ds-card-content>
<div class="stat">
<div class="stat__label">Typography</div>
<div class="stat__value">${stats.typography}</div>
</div>
</ds-card-content>
</ds-card>
</div>
<!-- Actions -->
<div class="flex justify-between items-center mt-6 mb-4">
<div class="flex gap-3">
<ds-button data-variant="primary" data-action="extractTokens" ${this.store.isLoading('extractTokens') ? 'loading' : ''}>
Extract from Figma
</ds-button>
<ds-button data-variant="outline" data-action="syncTokens" ${this.store.isLoading('syncTokens') ? 'loading' : ''}>
Sync to Files
</ds-button>
</div>
<div class="flex gap-2 items-center">
<span class="text-sm text-muted">Export as:</span>
<ds-button data-variant="${exportFormat === 'css' ? 'primary' : 'ghost'}" data-size="sm" data-action="setExportFormat" data-format="css">CSS</ds-button>
<ds-button data-variant="${exportFormat === 'scss' ? 'primary' : 'ghost'}" data-size="sm" data-action="setExportFormat" data-format="scss">SCSS</ds-button>
<ds-button data-variant="${exportFormat === 'json' ? 'primary' : 'ghost'}" data-size="sm" data-action="setExportFormat" data-format="json">JSON</ds-button>
<ds-button data-variant="${exportFormat === 'ts' ? 'primary' : 'ghost'}" data-size="sm" data-action="setExportFormat" data-format="ts">TypeScript</ds-button>
</div>
</div>
${tokens.length > 0 ? sortedCategories.map(category => {
const categoryTokens = byCategory[category];
const categoryName = category.charAt(0).toUpperCase() + category.slice(1);
const isColor = category === 'color';
return `
<ds-card class="mt-4">
<ds-card-header>
<ds-card-title>${categoryName}</ds-card-title>
<ds-badge data-variant="outline">${categoryTokens.length} tokens</ds-badge>
</ds-card-header>
<ds-card-content>
${isColor ? `
<div class="grid grid-cols-6 gap-3">
${categoryTokens.map(token => `
<div class="token-swatch" title="${token.name}: ${token.value}">
<div class="token-swatch__color" style="background: ${token.value}; height: 48px; border-radius: var(--radius) var(--radius) 0 0; border: 1px solid var(--border);"></div>
<div class="token-swatch__info p-2 rounded-b text-center" style="background: var(--muted); border: 1px solid var(--border); border-top: 0;">
<div class="text-xs font-medium truncate">--${token.name}</div>
<div class="text-xs text-muted font-mono">${token.value}</div>
</div>
</div>
`).join('')}
</div>
` : `
<div class="grid grid-cols-4 gap-3">
${categoryTokens.map(token => `
<div class="flex items-center gap-3 p-3 rounded" style="background: var(--muted)">
${category === 'spacing' || category === 'sizing' ? `
<div style="width: ${Math.min(parseInt(token.value) || 16, 48)}px; height: 16px; background: var(--primary); border-radius: 2px; flex-shrink: 0;"></div>
` : category === 'radius' ? `
<div style="width: 24px; height: 24px; background: var(--primary); border-radius: ${token.value}; flex-shrink: 0;"></div>
` : category === 'shadow' ? `
<div style="width: 24px; height: 24px; background: var(--card); box-shadow: ${token.value}; border-radius: var(--radius-sm); flex-shrink: 0;"></div>
` : ''}
<div class="flex flex-col min-w-0">
<span class="text-sm font-medium truncate">--${token.name}</span>
<span class="text-xs text-muted font-mono truncate">${token.value}</span>
</div>
</div>
`).join('')}
</div>
`}
</ds-card-content>
</ds-card>
`}).join('') : `
<ds-card class="mt-4">
<ds-card-content>
<div class="text-center p-8">
<svg class="mx-auto mb-4 opacity-50" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10"/>
<circle cx="12" cy="12" r="4"/>
</svg>
<p class="text-muted mb-4">No tokens extracted yet.</p>
<p class="text-sm text-muted mb-4">Connect to Figma and extract design tokens to see them here.</p>
<ds-button data-variant="primary" data-action="extractTokens">
Extract Tokens from Figma
</ds-button>
</div>
</ds-card-content>
</ds-card>
`}
`;
}
renderComponents() {
const components = this.store.get('components') || [];
const selectedFramework = this.store.get('componentFramework') || 'react';
const generatedCode = this.store.get('generatedCode');
const frameworks = [
{ id: 'react', name: 'React', icon: 'R' },
{ id: 'vue', name: 'Vue', icon: 'V' },
{ id: 'webcomponent', name: 'Web Component', icon: 'W' },
{ id: 'svelte', name: 'Svelte', icon: 'S' }
];
return `
<div class="page-header">
<h1>Components</h1>
<p class="text-muted">View extracted components and generate code</p>
</div>
<!-- Stats -->
<div class="grid grid-cols-4 gap-4 mt-6">
<ds-card>
<ds-card-content>
<div class="stat">
<div class="stat__label">Total Components</div>
<div class="stat__value">${components.length}</div>
</div>
</ds-card-content>
</ds-card>
<ds-card>
<ds-card-content>
<div class="stat">
<div class="stat__label">With Variants</div>
<div class="stat__value">${components.filter(c => c.variants?.length > 0).length}</div>
</div>
</ds-card-content>
</ds-card>
<ds-card>
<ds-card-content>
<div class="stat">
<div class="stat__label">With Properties</div>
<div class="stat__value">${components.filter(c => c.properties?.length > 0).length}</div>
</div>
</ds-card-content>
</ds-card>
<ds-card>
<ds-card-content>
<div class="stat">
<div class="stat__label">Target Framework</div>
<div class="stat__value text-lg">${frameworks.find(f => f.id === selectedFramework)?.name || 'React'}</div>
</div>
</ds-card-content>
</ds-card>
</div>
<!-- Actions -->
<div class="flex justify-between items-center mt-6 mb-4">
<div class="flex gap-3">
<ds-button data-variant="primary" data-action="extractComponents" ${this.store.isLoading('extractComponents') ? 'loading' : ''}>
Extract from Figma
</ds-button>
<ds-button data-variant="outline" data-action="validateComponents" ${this.store.isLoading('validate') ? 'loading' : ''}>
Validate All
</ds-button>
</div>
<div class="flex gap-2 items-center">
<span class="text-sm text-muted">Generate for:</span>
${frameworks.map(fw => `
<ds-button
data-variant="${selectedFramework === fw.id ? 'primary' : 'ghost'}"
data-size="sm"
data-action="setFramework"
data-framework="${fw.id}"
title="${fw.name}"
>${fw.icon}</ds-button>
`).join('')}
</div>
</div>
${generatedCode ? `
<ds-card class="mb-4">
<ds-card-header>
<ds-card-title>Generated Code: ${generatedCode.component}</ds-card-title>
<ds-button data-variant="ghost" data-size="sm" data-action="closeGeneratedCode">Close</ds-button>
</ds-card-header>
<ds-card-content>
<pre class="p-4 rounded overflow-x-auto text-sm" style="background: var(--muted); font-family: monospace; max-height: 400px;"><code>${this.escapeHtml(generatedCode.code)}</code></pre>
</ds-card-content>
<ds-card-footer>
<ds-button data-variant="outline" data-size="sm" data-action="copyCode">Copy to Clipboard</ds-button>
</ds-card-footer>
</ds-card>
` : ''}
${components.length > 0 ? `
<div class="grid grid-cols-3 gap-4">
${components.map(comp => `
<ds-card class="component-card">
<ds-card-header>
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded flex items-center justify-center text-sm font-bold" style="background: var(--primary); color: var(--primary-foreground);">
${comp.name.charAt(0).toUpperCase()}
</div>
<ds-card-title>${comp.name}</ds-card-title>
</div>
</ds-card-header>
<ds-card-content>
<p class="text-sm text-muted mb-3">${comp.description || 'Component from Figma'}</p>
${comp.variants?.length > 0 ? `
<div class="mb-3">
<div class="text-xs text-muted mb-1">Variants:</div>
<div class="flex flex-wrap gap-1">
${comp.variants.slice(0, 5).map(v => `<ds-badge data-variant="secondary" data-size="sm">${v}</ds-badge>`).join('')}
${comp.variants.length > 5 ? `<ds-badge data-variant="outline" data-size="sm">+${comp.variants.length - 5}</ds-badge>` : ''}
</div>
</div>
` : ''}
${comp.properties?.length > 0 ? `
<div class="mb-3">
<div class="text-xs text-muted mb-1">Properties:</div>
<div class="flex flex-wrap gap-1">
${comp.properties.slice(0, 4).map(p => `<ds-badge data-variant="outline" data-size="sm">${p.name}: ${p.type}</ds-badge>`).join('')}
${comp.properties.length > 4 ? `<ds-badge data-variant="outline" data-size="sm">+${comp.properties.length - 4}</ds-badge>` : ''}
</div>
</div>
` : ''}
<div class="flex justify-between text-xs text-muted">
<span>Key: ${comp.key?.slice(0, 8) || 'N/A'}...</span>
</div>
</ds-card-content>
<ds-card-footer>
<div class="flex gap-2">
<ds-button data-variant="primary" data-size="sm" data-action="generateCode" data-component="${comp.name}" ${this.store.isLoading('generateCode') ? 'loading' : ''}>
Generate ${frameworks.find(f => f.id === selectedFramework)?.name || 'Code'}
</ds-button>
<ds-button data-variant="ghost" data-size="sm" data-action="viewComponent" data-component="${comp.name}">
Details
</ds-button>
</div>
</ds-card-footer>
</ds-card>
`).join('')}
</div>
` : `
<ds-card>
<ds-card-content>
<div class="text-center p-8">
<svg class="mx-auto mb-4 opacity-50" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M3 9h18"/>
<path d="M9 21V9"/>
</svg>
<p class="text-muted mb-4">No components extracted yet.</p>
<p class="text-sm text-muted mb-4">Extract components from your Figma file to generate code.</p>
<ds-button data-variant="primary" data-action="extractComponents">
Extract Components from Figma
</ds-button>
</div>
</ds-card-content>
</ds-card>
`}
`;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
renderFigma() {
const figmaFileKey = this.store.get('figmaFileKey') || '';
const selectedProject = this.store.get('selectedProject');
const projectDescription = selectedProject?.description || '';
const lastSync = this.store.get('lastSync');
return `
<div class="page-header">
<h1>Project Settings</h1>
<p class="text-muted">Configure your project and Figma integration</p>
</div>
<div class="grid grid-cols-2 gap-6 mt-6">
<ds-card>
<ds-card-header>
<ds-card-title>Project Details</ds-card-title>
<ds-card-description>Describe your design system project</ds-card-description>
</ds-card-header>
<ds-card-content>
<ds-input
id="project-description"
label="Project Description"
placeholder="Brief description of this design system"
value="${this.escapeHtml(projectDescription)}"
></ds-input>
</ds-card-content>
<ds-card-footer>
<ds-button data-variant="primary" data-action="saveProjectDescription">
Save Description
</ds-button>
</ds-card-footer>
</ds-card>
<ds-card>
<ds-card-header>
<ds-card-title>Figma Connection</ds-card-title>
<ds-card-description>Configure your Figma file</ds-card-description>
</ds-card-header>
<ds-card-content>
<ds-input
id="figma-file-key"
label="Figma File Key"
placeholder="Enter your Figma file key"
value="${figmaFileKey}"
></ds-input>
<p class="text-xs text-muted mt-2">Find this in your Figma URL: figma.com/file/<strong>[FILE_KEY]</strong>/...</p>
</ds-card-content>
<ds-card-footer>
<ds-button data-variant="primary" data-action="saveFigmaKey">
Save & Connect
</ds-button>
</ds-card-footer>
</ds-card>
<ds-card>
<ds-card-header>
<ds-card-title>Sync Status</ds-card-title>
</ds-card-header>
<ds-card-content>
<div class="flex flex-col gap-3">
<div class="flex justify-between items-center">
<span>Connection:</span>
<ds-badge data-variant="${figmaFileKey ? 'success' : 'secondary'}" dot>
${figmaFileKey ? 'Connected' : 'Not configured'}
</ds-badge>
</div>
<div class="flex justify-between items-center">
<span>Last Sync:</span>
<span class="text-muted">${lastSync ? this.formatTime(lastSync) : 'Never'}</span>
</div>
</div>
</ds-card-content>
</ds-card>
</div>
<ds-card class="mt-6">
<ds-card-header>
<ds-card-title>Available Tools</ds-card-title>
<ds-card-description>Figma integration capabilities</ds-card-description>
</ds-card-header>
<ds-card-content>
<div class="grid grid-cols-2 gap-4">
<div class="p-4 rounded" style="background: var(--muted)">
<h4 class="font-medium mb-1">Extract Variables</h4>
<p class="text-sm text-muted">Pull design tokens from Figma variables</p>
</div>
<div class="p-4 rounded" style="background: var(--muted)">
<h4 class="font-medium mb-1">Extract Components</h4>
<p class="text-sm text-muted">Get component definitions and variants</p>
</div>
<div class="p-4 rounded" style="background: var(--muted)">
<h4 class="font-medium mb-1">Extract Styles</h4>
<p class="text-sm text-muted">Pull text, color, and effect styles</p>
</div>
<div class="p-4 rounded" style="background: var(--muted)">
<h4 class="font-medium mb-1">Sync Tokens</h4>
<p class="text-sm text-muted">Sync tokens to CSS/SCSS/JSON files</p>
</div>
<div class="p-4 rounded" style="background: var(--muted)">
<h4 class="font-medium mb-1">Visual Diff</h4>
<p class="text-sm text-muted">Compare visual changes between versions</p>
</div>
<div class="p-4 rounded" style="background: var(--muted)">
<h4 class="font-medium mb-1">Validate Components</h4>
<p class="text-sm text-muted">Check components against naming rules</p>
</div>
<div class="p-4 rounded" style="background: var(--muted)">
<h4 class="font-medium mb-1">Generate Code</h4>
<p class="text-sm text-muted">Create component code from designs</p>
</div>
</div>
</ds-card-content>
</ds-card>
`;
}
renderTeams() {
const user = this.store.get('user');
const role = this.store.get('role');
return `
<div class="page-header">
<h1>Team Management</h1>
<p class="text-muted">Manage teams and permissions</p>
</div>
<div class="flex gap-3 mt-6 mb-4">
${this.store.hasPermission('manage_team_members') ? `
<ds-button data-variant="primary" data-action="createTeam">
Create Team
</ds-button>
` : ''}
</div>
<div class="grid grid-cols-3 gap-4 mt-4">
<ds-card>
<ds-card-header>
<ds-card-title>Design System Core</ds-card-title>
<ds-badge data-variant="success" dot>Active</ds-badge>
</ds-card-header>
<ds-card-content>
<div class="flex flex-col gap-2 text-sm">
<div class="flex justify-between">
<span class="text-muted">Members:</span>
<span>5</span>
</div>
<div class="flex justify-between">
<span class="text-muted">Projects:</span>
<span>3</span>
</div>
<div class="flex justify-between">
<span class="text-muted">Your Role:</span>
<ds-badge data-variant="outline">${role}</ds-badge>
</div>
</div>
</ds-card-content>
<ds-card-footer>
<ds-button data-variant="ghost" data-size="sm">View Details</ds-button>
</ds-card-footer>
</ds-card>
<ds-card>
<ds-card-header>
<ds-card-title>Product Team A</ds-card-title>
<ds-badge data-variant="secondary">Member</ds-badge>
</ds-card-header>
<ds-card-content>
<div class="flex flex-col gap-2 text-sm">
<div class="flex justify-between">
<span class="text-muted">Members:</span>
<span>8</span>
</div>
<div class="flex justify-between">
<span class="text-muted">Projects:</span>
<span>2</span>
</div>
</div>
</ds-card-content>
<ds-card-footer>
<ds-button data-variant="ghost" data-size="sm">View Details</ds-button>
</ds-card-footer>
</ds-card>
</div>
<ds-card class="mt-6">
<ds-card-header>
<ds-card-title>Role Permissions</ds-card-title>
</ds-card-header>
<ds-card-content>
<div class="grid grid-cols-4 gap-4 text-sm">
<div>
<h4 class="font-medium mb-2">Super Admin</h4>
<p class="text-muted">Full system access</p>
</div>
<div>
<h4 class="font-medium mb-2">Team Lead</h4>
<p class="text-muted">Manage team, sync, generate</p>
</div>
<div>
<h4 class="font-medium mb-2">Developer</h4>
<p class="text-muted">Read, write, sync</p>
</div>
<div>
<h4 class="font-medium mb-2">Viewer</h4>
<p class="text-muted">Read-only access</p>
</div>
</div>
</ds-card-content>
</ds-card>
`;
}
renderProjects() {
const projects = this.store.get('projects') || [];
const showCreateForm = this.store.get('showCreateProjectForm');
return `
<div class="page-header">
<h1>Projects</h1>
<p class="text-muted">Manage your design system projects</p>
</div>
<div class="flex justify-between items-center mt-6 mb-4">
<ds-input icon="search" placeholder="Search projects..." id="project-search"></ds-input>
<ds-button data-variant="primary" data-action="toggleCreateProject" ${this.store.isLoading('createProject') ? 'loading' : ''}>
${showCreateForm ? 'Cancel' : 'New Project'}
</ds-button>
</div>
${showCreateForm ? `
<ds-card class="mb-6">
<ds-card-header>
<ds-card-title>Create New Project</ds-card-title>
</ds-card-header>
<ds-card-content>
<ds-input
id="new-project-name"
label="Project Name"
placeholder="My Design System"
required
></ds-input>
</ds-card-content>
<ds-card-footer>
<ds-button data-variant="primary" data-action="submitCreateProject" ${this.store.isLoading('createProject') ? 'loading' : ''}>
Create Project
</ds-button>
</ds-card-footer>
</ds-card>
` : ''}
${projects.length > 0 ? `
<div class="grid grid-cols-2 gap-4">
${projects.map(p => `
<ds-card class="project-card" data-project-id="${p.id}">
<ds-card-header>
<ds-card-title>${p.name}</ds-card-title>
<ds-badge data-variant="${p.status === 'active' ? 'success' : 'secondary'}" dot>
${p.status || 'active'}
</ds-badge>
</ds-card-header>
<ds-card-content>
<p class="text-sm mb-3">${p.description || 'No description'}</p>
<div class="flex flex-col gap-2 text-sm">
<div class="flex justify-between">
<span class="text-muted">Figma Key:</span>
<span class="font-mono text-xs">${p.figma_file_key || 'Not configured'}</span>
</div>
<div class="flex justify-between">
<span class="text-muted">Last Sync:</span>
<span>${p.last_sync ? this.formatTime(p.last_sync) : 'Never'}</span>
</div>
<div class="flex justify-between">
<span class="text-muted">Created:</span>
<span>${p.created_at ? this.formatTime(p.created_at) : 'Unknown'}</span>
</div>
</div>
</ds-card-content>
<ds-card-footer>
<div class="flex gap-2">
<ds-button data-variant="primary" data-size="sm" data-action="selectProject" data-project-id="${p.id}">
Open
</ds-button>
<ds-button data-variant="outline" data-size="sm" data-action="syncProject" data-project-id="${p.id}" data-figma-key="${p.figma_file_key || ''}">
Sync Tokens
</ds-button>
<ds-button data-variant="ghost" data-size="sm" data-action="deleteProject" data-project-id="${p.id}">
Delete
</ds-button>
</div>
</ds-card-footer>
</ds-card>
`).join('')}
</div>
` : `
<ds-card>
<ds-card-content>
<div class="text-center p-8">
<svg class="mx-auto mb-4 opacity-50" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path d="M3 3h18v18H3z"/>
<path d="M21 9H3"/>
<path d="M9 21V9"/>
</svg>
<p class="text-muted mb-4">No projects yet. Create your first project to get started.</p>
<ds-button data-variant="primary" data-action="toggleCreateProject">
Create Your First Project
</ds-button>
</div>
</ds-card-content>
</ds-card>
`}
`;
}
renderSettings() {
const config = this.store.get('runtimeConfig') || {};
const services = this.store.get('services') || {};
const figmaConfig = this.store.get('figmaConfig') || {};
const mode = config.mode || 'local';
return `
<div class="page-header">
<h1>Settings</h1>
<p class="text-muted">Configure your design system server</p>
</div>
<div class="mt-6 flex flex-col gap-6">
<!-- DSS Mode -->
<ds-card>
<ds-card-header>
<ds-card-title>Server Mode</ds-card-title>
<ds-card-description>Choose how DSS operates</ds-card-description>
</ds-card-header>
<ds-card-content>
<div class="flex gap-4">
<div class="flex-1 p-4 rounded cursor-pointer ${mode === 'local' ? 'ring-2 ring-primary' : ''}"
style="background: var(--muted)" data-action="setMode" data-mode="local">
<h4 class="font-medium mb-1">Local Dev Companion</h4>
<p class="text-sm text-muted">Run alongside your project, provides UI dev assistance, component preview, and local services.</p>
</div>
<div class="flex-1 p-4 rounded cursor-pointer ${mode === 'server' ? 'ring-2 ring-primary' : ''}"
style="background: var(--muted)" data-action="setMode" data-mode="server">
<h4 class="font-medium mb-1">Remote Server</h4>
<p class="text-sm text-muted">Deployed centrally, serves design systems to teams, multi-project management.</p>
</div>
</div>
</ds-card-content>
</ds-card>
<!-- Figma Configuration -->
<ds-card>
<ds-card-header>
<ds-card-title>Figma Integration</ds-card-title>
<ds-card-description>Connect to Figma API</ds-card-description>
<ds-badge data-variant="${figmaConfig.configured ? 'success' : 'secondary'}" dot>
${figmaConfig.configured ? 'Connected' : 'Not configured'}
</ds-badge>
</ds-card-header>
<ds-card-content>
<div class="flex flex-col gap-4">
<ds-input
id="figma-token-input"
label="Figma Personal Access Token"
type="password"
placeholder="figd_xxxxxxxxxx"
value="${figmaConfig.configured ? '••••••••••••' : ''}"
></ds-input>
<p class="text-xs text-muted">
Get your token from <a href="https://www.figma.com/developers/api#access-tokens" target="_blank" class="text-primary">Figma Settings → Personal Access Tokens</a>
</p>
<div class="flex gap-2">
<ds-button data-variant="primary" data-action="saveFigmaToken" ${this.store.isLoading('saveFigmaToken') ? 'loading' : ''}>
Save Token
</ds-button>
<ds-button data-variant="outline" data-action="testFigmaConnection" ${this.store.isLoading('testFigma') ? 'loading' : ''}>
Test Connection
</ds-button>
</div>
${this.store.get('figmaTestResult') ? `
<div class="text-sm ${this.store.get('figmaTestResult').success ? 'text-success' : 'text-destructive'}">
${this.store.get('figmaTestResult').success ?
`Connected as ${this.store.get('figmaTestResult').user}` :
`Error: ${this.store.get('figmaTestResult').error}`}
</div>
` : ''}
</div>
</ds-card-content>
</ds-card>
<!-- Component Settings (Dynamic from Registry) -->
<ds-card>
<ds-card-header>
<ds-card-title>External Tools & Integrations</ds-card-title>
<ds-card-description>Configure connected tools and services</ds-card-description>
</ds-card-header>
<ds-card-content>
<div class="flex flex-col gap-4">
${this.renderComponentSettings()}
</div>
</ds-card-content>
</ds-card>
<!-- Services Discovery -->
<ds-card>
<ds-card-header>
<ds-card-title>Companion Services</ds-card-title>
<ds-card-description>Discovered and configured services</ds-card-description>
</ds-card-header>
<ds-card-content>
<div class="grid grid-cols-3 gap-4">
${this.renderServiceCard('storybook', 'Storybook', services)}
${this.renderServiceCard('vite', 'Vite Dev Server', services)}
${this.renderServiceCard('nextjs', 'Next.js', services)}
</div>
<div class="mt-4">
<ds-button data-variant="ghost" data-action="refreshServices">
Refresh Services
</ds-button>
</div>
</ds-card-content>
</ds-card>
<!-- Feature Toggles -->
<ds-card>
<ds-card-header>
<ds-card-title>Features</ds-card-title>
<ds-card-description>Enable or disable DSS features</ds-card-description>
</ds-card-header>
<ds-card-content>
<div class="flex flex-col gap-3">
${this.renderFeatureToggle('visual_qa', 'Visual QA', 'Compare Figma designs with implementation', config.features)}
${this.renderFeatureToggle('token_sync', 'Token Sync', 'Sync design tokens to code', config.features)}
${this.renderFeatureToggle('code_gen', 'Code Generation', 'Generate component code from Figma', config.features)}
${this.renderFeatureToggle('ai_advisor', 'AI Advisor', 'Get AI suggestions for design system improvements', config.features)}
</div>
</ds-card-content>
</ds-card>
<!-- Appearance -->
<ds-card>
<ds-card-header>
<ds-card-title>Appearance</ds-card-title>
<ds-card-description>Customize the interface</ds-card-description>
</ds-card-header>
<ds-card-content>
<div class="flex items-center justify-between">
<span>Dark Mode</span>
<ds-button data-variant="outline" data-size="sm" data-action="toggle-theme">
Toggle Theme
</ds-button>
</div>
</ds-card-content>
</ds-card>
<!-- Output Configuration -->
<ds-card>
<ds-card-header>
<ds-card-title>Output Configuration</ds-card-title>
<ds-card-description>Token and component generation settings</ds-card-description>
</ds-card-header>
<ds-card-content>
<div class="grid grid-cols-2 gap-4">
<ds-input label="Token Output Path" value="./admin-ui/css/tokens.css"></ds-input>
<ds-input label="Component Output Path" value="./admin-ui/js/components"></ds-input>
</div>
</ds-card-content>
</ds-card>
<!-- Danger Zone -->
<ds-card style="border-color: var(--destructive);">
<ds-card-header>
<ds-card-title style="color: var(--destructive);">⚠️ Danger Zone</ds-card-title>
<ds-card-description>Irreversible operations - use with caution</ds-card-description>
</ds-card-header>
<ds-card-content>
<div class="flex flex-col gap-4">
<div>
<h4 class="font-medium mb-2">Reset DSS to Fresh State</h4>
<p class="text-sm text-muted mb-3">
This will delete all user-created themes, cached data, and project databases.
The DSS structure and default themes will be preserved.
</p>
<ds-button data-variant="destructive" data-action="resetDSS" ${this.store.isLoading('resetDSS') ? 'loading' : ''}>
Reset DSS
</ds-button>
</div>
</div>
</ds-card-content>
</ds-card>
<!-- API Status -->
<ds-card>
<ds-card-header>
<ds-card-title>API Status</ds-card-title>
</ds-card-header>
<ds-card-content>
<div class="flex flex-col gap-2 text-sm">
<div class="flex justify-between">
<span>API Mode:</span>
<ds-badge data-variant="${this.store.get('useMock') ? 'warning' : 'success'}" dot>
${this.store.get('useMock') ? 'Mock (Backend unavailable)' : 'Live'}
</ds-badge>
</div>
<div class="flex justify-between">
<span>Base URL:</span>
<span class="text-muted">${window.location.hostname === 'localhost' ? 'http://localhost:3456/api' : '/api'}</span>
</div>
</div>
</ds-card-content>
</ds-card>
</div>
`;
}
renderPlugins() {
const plugins = pluginService.getAll();
const enabledCount = plugins.filter(p => p.enabled).length;
return `
<div class="page-header">
<h1>Plugins</h1>
<p class="text-muted">${enabledCount} of ${plugins.length} plugins enabled</p>
</div>
<div class="plugins-grid">
${plugins.map(plugin => this.renderPluginCard(plugin)).join('')}
</div>
<style>
.plugins-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
margin-top: 24px;
}
.plugin-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
transition: border-color 0.2s;
}
.plugin-card:hover {
border-color: var(--primary);
}
.plugin-card.enabled {
border-left: 3px solid var(--success);
}
.plugin-card__header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.plugin-card__icon {
font-size: 24px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--muted);
border-radius: 8px;
}
.plugin-card__title {
flex: 1;
}
.plugin-card__name {
font-weight: 600;
font-size: 15px;
}
.plugin-card__version {
font-size: 11px;
color: var(--text-muted);
}
.plugin-card__desc {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 12px;
line-height: 1.4;
}
.plugin-card__meta {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.plugin-card__author {
font-size: 11px;
color: var(--text-muted);
}
.plugin-toggle {
position: relative;
width: 40px;
height: 22px;
background: var(--muted);
border-radius: 11px;
cursor: pointer;
transition: background 0.2s;
}
.plugin-toggle.enabled {
background: var(--success);
}
.plugin-toggle::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
background: white;
border-radius: 50%;
transition: transform 0.2s;
}
.plugin-toggle.enabled::after {
transform: translateX(18px);
}
.plugin-card__settings {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.plugin-setting {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
}
.plugin-setting__label {
font-size: 13px;
}
.plugin-setting select {
padding: 4px 8px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--background);
font-size: 12px;
}
</style>
`;
}
renderPluginCard(plugin) {
const settings = pluginService.getPluginSettings(plugin.id);
return `
<div class="plugin-card ${plugin.enabled ? 'enabled' : ''}" data-plugin-id="${plugin.id}">
<div class="plugin-card__header">
<div class="plugin-card__icon">${plugin.icon}</div>
<div class="plugin-card__title">
<div class="plugin-card__name">${plugin.name}</div>
<div class="plugin-card__version">v${plugin.version}</div>
</div>
<div class="plugin-toggle ${plugin.enabled ? 'enabled' : ''}"
data-action="togglePlugin"
data-plugin="${plugin.id}"
title="${plugin.enabled ? 'Disable' : 'Enable'} plugin">
</div>
</div>
<div class="plugin-card__desc">${plugin.description}</div>
${plugin.enabled && plugin.settings.length > 0 ? `
<div class="plugin-card__settings">
${plugin.settings.map(setting => this.renderPluginSetting(plugin.id, setting, settings)).join('')}
</div>
` : ''}
<div class="plugin-card__meta">
<span class="plugin-card__author">by ${plugin.author}</span>
<ds-badge data-variant="${plugin.enabled ? 'success' : 'secondary'}" data-size="sm">
${plugin.enabled ? 'Enabled' : 'Disabled'}
</ds-badge>
</div>
</div>
`;
}
renderPluginSetting(pluginId, setting, currentSettings) {
const value = currentSettings[setting.key] ?? setting.default;
if (setting.type === 'select') {
return `
<div class="plugin-setting">
<span class="plugin-setting__label">${setting.label}</span>
<select data-action="setPluginSetting" data-plugin="${pluginId}" data-key="${setting.key}">
${setting.options.map(opt => `
<option value="${opt.value}" ${value === opt.value ? 'selected' : ''}>${opt.label}</option>
`).join('')}
</select>
</div>
`;
}
if (setting.type === 'boolean') {
return `
<div class="plugin-setting">
<span class="plugin-setting__label">${setting.label}</span>
<div class="plugin-toggle ${value ? 'enabled' : ''}"
data-action="setPluginSetting"
data-plugin="${pluginId}"
data-key="${setting.key}"
data-type="boolean">
</div>
</div>
`;
}
return '';
}
renderDocs() {
return `
<div class="page-header">
<h1>Documentation</h1>
<p class="text-muted">DSS usage guides for all teams</p>
</div>
<div class="docs-layout mt-6">
<nav class="docs-nav">
<div class="docs-nav__section">
<div class="docs-nav__title">Getting Started</div>
<a class="docs-nav__link active" data-doc="overview">Overview</a>
<a class="docs-nav__link" data-doc="quickstart">Quick Start</a>
<a class="docs-nav__link" data-doc="concepts">Key Concepts</a>
</div>
<div class="docs-nav__section">
<div class="docs-nav__title">Team Guides</div>
<a class="docs-nav__link" data-doc="ui-team">UI Team</a>
<a class="docs-nav__link" data-doc="ux-team">UX Team</a>
<a class="docs-nav__link" data-doc="qa-team">QA Team</a>
</div>
<div class="docs-nav__section">
<div class="docs-nav__title">Features</div>
<a class="docs-nav__link" data-doc="tokens">Design Tokens</a>
<a class="docs-nav__link" data-doc="figma">Figma Integration</a>
<a class="docs-nav__link" data-doc="components">Components</a>
<a class="docs-nav__link" data-doc="ai-chat">AI Chat</a>
</div>
<div class="docs-nav__section">
<div class="docs-nav__title">Reference</div>
<a class="docs-nav__link" href="/api/docs" target="_blank">API Docs ↗</a>
<a class="docs-nav__link" data-doc="cli">CLI Commands</a>
<a class="docs-nav__link" data-doc="troubleshooting">Troubleshooting</a>
</div>
</nav>
<div class="docs-content" id="docs-content">
${this.getDocContent('overview')}
</div>
</div>
`;
}
getDocContent(docId) {
const docs = {
overview: `
<h2>What is DSS?</h2>
<p>Design System Server (DSS) is a platform that helps teams manage, sync, and evolve their design systems by connecting Figma designs to code.</p>
<h3>Core Features</h3>
<ul>
<li><strong>Token Extraction</strong> — Pull design tokens from Figma variables</li>
<li><strong>Token Sync</strong> — Generate CSS/JSON from Figma tokens</li>
<li><strong>Component Analysis</strong> — Scan your codebase for components</li>
<li><strong>Visual Diff</strong> — Detect changes between Figma versions</li>
<li><strong>AI Assistant</strong> — Get help via the built-in chat</li>
</ul>
<h3>Architecture</h3>
<ul>
<li><strong>REST API</strong> — Port 3456 (python tools/api/server.py)</li>
<li><strong>MCP Server</strong> — Port 3457 for AI tools</li>
<li><strong>Admin UI</strong> — This dashboard</li>
<li><strong>CLI</strong> — Command-line interface</li>
</ul>
`,
quickstart: `
<h2>Quick Start</h2>
<h3>1. Start the Server</h3>
<pre><code>cd apps/dss
python tools/api/server.py</code></pre>
<h3>2. Create a Project</h3>
<p>Go to <a href="#projects">Projects</a> → Create Project</p>
<h3>3. Add Figma File</h3>
<p>In your project settings, paste your Figma file key:</p>
<pre><code>https://www.figma.com/file/<strong>FILE_KEY</strong>/...</code></pre>
<h3>4. Extract Tokens</h3>
<p>Use the dashboard or CLI:</p>
<pre><code>dss ingest figma YOUR_FILE_KEY</code></pre>
<h3>5. Sync to Code</h3>
<p>Click "Sync Tokens" or:</p>
<pre><code>dss sync tokens --output ./tokens.css</code></pre>
`,
concepts: `
<h2>Key Concepts</h2>
<h3>Design Tokens</h3>
<p>Tokens are the atomic values of your design system: colors, spacing, typography. DSS extracts these from Figma variables and converts them to CSS custom properties or JSON.</p>
<h3>Translation Dictionaries</h3>
<p>DSS uses a canonical internal structure. When importing from external sources, translation dictionaries map external names to DSS standard names.</p>
<h3>Token Drift</h3>
<p>When code diverges from Figma designs, that's "drift". The UI Team dashboard tracks drift issues and helps resolve them.</p>
<h3>ESRE (Expected State, Real State, Evidence)</h3>
<p>QA team uses ESRE to define test cases: what should happen, what actually happens, and proof.</p>
`,
'ui-team': `
<h2>UI Team Guide</h2>
<p>As a UI developer, you'll use DSS to keep code in sync with designs.</p>
<h3>Daily Workflow</h3>
<ol>
<li>Check the <a href="#dashboard">Dashboard</a> for token drift alerts</li>
<li>Run token sync when Figma updates</li>
<li>Generate component code from new Figma components</li>
<li>Review and resolve drift issues</li>
</ol>
<h3>Key Tools</h3>
<ul>
<li><strong>Extract Tokens</strong> — Pull latest from Figma</li>
<li><strong>Sync Tokens</strong> — Update CSS variables</li>
<li><strong>Generate Code</strong> — Create React/Web Components</li>
<li><strong>Token Drift</strong> — Track code/design mismatches</li>
</ul>
<h3>CLI Commands</h3>
<pre><code>dss ingest figma FILE_KEY
dss sync tokens -o ./tokens.css
dss analyze ./src</code></pre>
`,
'ux-team': `
<h2>UX Team Guide</h2>
<p>As a UX designer, DSS helps you maintain design consistency and validate implementations.</p>
<h3>Daily Workflow</h3>
<ol>
<li>Add Figma files to projects</li>
<li>Run visual diff after design changes</li>
<li>Review token consistency reports</li>
<li>Validate component implementations</li>
</ol>
<h3>Key Tools</h3>
<ul>
<li><strong>Figma Files</strong> — Manage connected design files</li>
<li><strong>Visual Diff</strong> — Compare Figma versions</li>
<li><strong>Token Validation</strong> — Ensure token consistency</li>
<li><strong>Component Validation</strong> — Check naming conventions</li>
</ul>
<h3>Best Practices</h3>
<ul>
<li>Use Figma variables for all tokens</li>
<li>Follow component naming conventions</li>
<li>Document component variants</li>
<li>Run visual diff before handoff</li>
</ul>
`,
'qa-team': `
<h2>QA Team Guide</h2>
<p>DSS helps QA teams validate design system implementations.</p>
<h3>Daily Workflow</h3>
<ol>
<li>Review visual regression reports</li>
<li>Define ESRE test cases for components</li>
<li>Run component validation</li>
<li>Export audit logs for compliance</li>
</ol>
<h3>ESRE Testing</h3>
<p>Define expected behaviors:</p>
<ul>
<li><strong>Expected State</strong> — What should happen</li>
<li><strong>Real State</strong> — What actually happens</li>
<li><strong>Evidence</strong> — Screenshots, logs, recordings</li>
</ul>
<h3>Key Tools</h3>
<ul>
<li><strong>Component Validation</strong> — Automated checks</li>
<li><strong>Visual Diff</strong> — Regression detection</li>
<li><strong>Audit Log</strong> — Track all changes</li>
<li><strong>ESRE Definitions</strong> — Test case management</li>
</ul>
`,
tokens: `
<h2>Design Tokens</h2>
<h3>Token Categories</h3>
<ul>
<li><strong>Colors</strong> — Primary, secondary, semantic colors</li>
<li><strong>Spacing</strong> — Margin, padding scales</li>
<li><strong>Typography</strong> — Font sizes, weights, line heights</li>
<li><strong>Radius</strong> — Border radius values</li>
<li><strong>Shadows</strong> — Box shadow definitions</li>
</ul>
<h3>Export Formats</h3>
<ul>
<li><strong>CSS</strong> — Custom properties (:root { --color-primary: ... })</li>
<li><strong>JSON</strong> — Structured token data</li>
<li><strong>SCSS</strong> — Sass variables</li>
</ul>
<h3>Token Naming</h3>
<pre><code>--{category}-{name}[-{variant}]
Examples:
--color-primary
--space-4
--text-lg
--radius-md</code></pre>
`,
figma: `
<h2>Figma Integration</h2>
<h3>Setup</h3>
<ol>
<li>Get a Personal Access Token from <a href="https://www.figma.com/developers/api#access-tokens" target="_blank">Figma Settings</a></li>
<li>Go to <a href="#settings">Settings</a> → Figma Integration</li>
<li>Paste and save your token</li>
<li>Test the connection</li>
</ol>
<h3>File Key</h3>
<p>Extract from your Figma URL:</p>
<pre><code>https://www.figma.com/file/<strong>ABC123</strong>/My-Design
↑ This is your file key</code></pre>
<h3>What Gets Extracted</h3>
<ul>
<li><strong>Variables</strong> → Design tokens</li>
<li><strong>Components</strong> → Component metadata</li>
<li><strong>Styles</strong> → Text and color styles</li>
</ul>
`,
components: `
<h2>Components</h2>
<h3>Component Analysis</h3>
<p>DSS scans your codebase to find React and Web Components:</p>
<pre><code>dss analyze ./src</code></pre>
<h3>Code Generation</h3>
<p>Generate component code from Figma:</p>
<ul>
<li><strong>React</strong> — Functional components with CSS modules</li>
<li><strong>Web Components</strong> — Custom elements with Shadow DOM</li>
</ul>
<h3>Storybook Integration</h3>
<p>Generate stories for your components:</p>
<pre><code>dss storybook generate ./src/components</code></pre>
`,
'ai-chat': `
<h2>AI Chat</h2>
<p>The AI assistant helps with design system tasks.</p>
<h3>What It Can Do</h3>
<ul>
<li>Answer questions about your design system</li>
<li>Suggest improvements</li>
<li>Help debug issues</li>
<li>Execute DSS tools</li>
</ul>
<h3>Example Prompts</h3>
<ul>
<li>"Extract tokens from my Figma file"</li>
<li>"Show me quick wins for my codebase"</li>
<li>"What's the status of my project?"</li>
<li>"Help me fix token drift issues"</li>
</ul>
<h3>Tool Execution</h3>
<p>The AI can run DSS tools directly. Look for tool suggestions in responses and click to execute.</p>
`,
cli: `
<h2>CLI Commands</h2>
<h3>Installation</h3>
<pre><code># Add to PATH
export PATH="$PATH:/path/to/dss/bin"
# Or use directly
./bin/dss --help</code></pre>
<h3>Commands</h3>
<pre><code><strong>dss init</strong>
Initialize DSS in current directory
<strong>dss ingest figma FILE_KEY</strong>
Extract tokens from Figma file
<strong>dss sync tokens -o PATH</strong>
Sync tokens to CSS file
<strong>dss analyze PATH</strong>
Analyze codebase for components
<strong>dss storybook generate PATH</strong>
Generate Storybook stories
<strong>dss start</strong>
Start DSS server</code></pre>
`,
troubleshooting: `
<h2>Troubleshooting</h2>
<h3>Server Won't Start</h3>
<pre><code># Check Python version (need 3.10+)
python --version
# Install dependencies
pip install -r requirements.txt
# Check port availability
lsof -i :3456</code></pre>
<h3>Figma Connection Failed</h3>
<ul>
<li>Verify token hasn't expired</li>
<li>Check token has read access to the file</li>
<li>Ensure file key is correct (not the full URL)</li>
</ul>
<h3>Tokens Not Syncing</h3>
<ul>
<li>Confirm Figma file has variables defined</li>
<li>Check output path is writable</li>
<li>Look for errors in browser console</li>
</ul>
<h3>API Errors</h3>
<ul>
<li>Check server is running: <code>curl http://localhost:3456/health</code></li>
<li>Review server logs for details</li>
<li>Verify CORS settings for remote access</li>
</ul>
<h3>Getting Help</h3>
<ul>
<li>Use the AI Chat for guidance</li>
<li>Check logs: Settings → Debug Console</li>
<li>Export logs for support</li>
</ul>
`
};
return docs[docId] || docs.overview;
}
renderServiceCard(serviceId, serviceName, services) {
const discovered = services.discovered?.[serviceId] || {};
const configured = services.configured?.[serviceId] || {};
const running = discovered.running || false;
return `
<div class="p-4 rounded" style="background: var(--muted)">
<div class="flex items-center justify-between mb-2">
<h4 class="font-medium">${serviceName}</h4>
<span class="status-dot ${running ? 'status-dot--success' : ''}"></span>
</div>
<p class="text-sm text-muted mb-2">
${running ? `Running on :${discovered.port}` : 'Not detected'}
</p>
${running ? `
<ds-button data-variant="ghost" data-size="sm" onclick="window.open('${discovered.url}', '_blank')">
Open
</ds-button>
` : ''}
</div>
`;
}
/**
* Render component settings from the registry
* Generates dynamic UI for all enabled components
*/
renderComponentSettings() {
let dssHost = 'localhost';
try {
dssHost = getDssHost();
} catch {
// Config not loaded yet
}
const components = getEnabledComponents();
return components.map(component => `
<div class="p-4 rounded border" style="background: var(--muted); border-color: var(--border)">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<span class="font-medium">${component.name}</span>
<ds-badge data-variant="secondary" data-size="sm">${component.category}</ds-badge>
</div>
${component.getUrl() ? `
<ds-button data-variant="ghost" data-size="sm" onclick="window.open('${component.getUrl()}', '_blank')">
Open
</ds-button>
` : ''}
</div>
<p class="text-sm text-muted mb-3">${component.description}</p>
${component.id === 'storybook' ? `
<div class="text-xs text-muted mb-3">
URL: <code>${component.getUrl() || 'Not configured'}</code>
<br>Host from server config: <code>${dssHost}</code>
</div>
<div class="flex gap-2">
<ds-button data-variant="primary" data-size="sm" data-action="initStorybook">
Initialize Storybook
</ds-button>
<ds-button data-variant="outline" data-size="sm" data-action="clearStorybook">
Clear Stories
</ds-button>
</div>
` : ''}
${component.id === 'figma' ? `
<div class="text-xs text-muted">
Token status: Check connection above
</div>
` : ''}
</div>
`).join('');
}
renderFeatureToggle(featureId, name, description, features = {}) {
const enabled = features?.[featureId] !== false;
return `
<div class="flex items-center justify-between p-3 rounded" style="background: var(--muted)">
<div>
<div class="font-medium">${name}</div>
<div class="text-sm text-muted">${description}</div>
</div>
<ds-button
data-variant="${enabled ? 'primary' : 'outline'}"
data-size="sm"
data-action="toggleFeature"
data-feature="${featureId}"
>
${enabled ? 'Enabled' : 'Disabled'}
</ds-button>
</div>
`;
}
/**
* Render ingestion result panel (after parsing a prompt)
*/
renderIngestionResult() {
const result = this.store.get('ingestionResult');
const browsing = this.store.get('browsingDesignSystems');
const systems = this.store.get('designSystems') || [];
// If browsing design systems
if (browsing) {
return `
<div class="mt-4 p-4 rounded border" style="background: var(--muted); border-color: var(--border)">
<div class="flex items-center justify-between mb-3">
<h4 class="font-medium">Available Design Systems</h4>
<ds-button data-variant="ghost" data-size="sm" data-action="closeBrowse">Close</ds-button>
</div>
<div class="grid grid-cols-3 gap-3" style="max-height: 300px; overflow-y: auto;">
${systems.map(sys => `
<div class="p-3 rounded border cursor-pointer hover:border-primary"
style="background: var(--background); border-color: var(--border)"
data-action="selectDesignSystem" data-system-id="${sys.id}">
<div class="font-medium text-sm">${sys.name}</div>
<div class="text-xs text-muted mt-1">${sys.description?.slice(0, 60) || ''}...</div>
<div class="flex gap-1 mt-2">
<ds-badge data-variant="secondary" data-size="sm">${sys.category || 'library'}</ds-badge>
${sys.framework ? `<ds-badge data-variant="outline" data-size="sm">${sys.framework}</ds-badge>` : ''}
</div>
</div>
`).join('')}
</div>
</div>
`;
}
if (!result) return '';
const { intent, sources, next_steps, suggestions } = result;
// Found a design system to ingest
if (sources?.length > 0 && sources[0].matched_system) {
const system = sources[0].matched_system;
return `
<div class="mt-4 p-4 rounded border" style="background: var(--muted); border-color: var(--border)">
<div class="flex items-center justify-between mb-3">
<div>
<h4 class="font-medium">${system.name}</h4>
<p class="text-sm text-muted">${system.description}</p>
</div>
<ds-badge data-variant="primary">${system.category || 'library'}</ds-badge>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<div class="text-xs text-muted mb-1">npm packages</div>
<code class="text-sm">${(system.npm_packages || []).join(', ') || 'N/A'}</code>
</div>
<div>
<div class="text-xs text-muted mb-1">Primary method</div>
<span class="text-sm">${system.primary_ingestion || 'npm'}</span>
</div>
</div>
${system.figma_community_url ? `
<div class="text-xs mb-3">
<a href="${system.figma_community_url}" target="_blank" class="text-primary">View Figma Community Kit</a>
</div>
` : ''}
<div class="flex gap-2">
<ds-button data-variant="primary" data-size="sm" data-action="confirmIngestion" data-system-id="${system.id}">
Ingest ${system.name}
</ds-button>
<ds-button data-variant="outline" data-size="sm" data-action="showAlternatives" data-system-id="${system.id}">
Other Methods
</ds-button>
<ds-button data-variant="ghost" data-size="sm" data-action="clearIngestion">
Cancel
</ds-button>
</div>
</div>
`;
}
// Show next steps or suggestions
if (next_steps?.length > 0) {
const step = next_steps[0];
if (step.action === 'request_source') {
return `
<div class="mt-4 p-4 rounded border" style="background: var(--muted); border-color: var(--border)">
<h4 class="font-medium mb-2">${step.message}</h4>
<div class="grid grid-cols-2 gap-3">
${(step.alternatives || []).map(alt => `
<div class="p-3 rounded border cursor-pointer hover:border-primary"
style="background: var(--background); border-color: var(--border)"
data-action="selectIngestionMethod" data-method="${alt.requires}">
<div class="font-medium text-sm">${alt.name}</div>
<div class="text-xs text-muted mt-1">${alt.description}</div>
</div>
`).join('')}
</div>
<ds-button data-variant="ghost" data-size="sm" class="mt-3" data-action="clearIngestion">
Cancel
</ds-button>
</div>
`;
}
if (step.action === 'search_npm') {
return `
<div class="mt-4 p-4 rounded border" style="background: var(--muted); border-color: var(--border)">
<div class="flex items-center gap-2 mb-2">
<span class="status-dot status-dot--warning"></span>
<span class="font-medium">Not found in registry</span>
</div>
<p class="text-sm text-muted mb-3">${step.message}</p>
<ds-button data-variant="primary" data-size="sm" data-action="searchNpm" data-query="${step.query || step.package}">
Search npm
</ds-button>
<ds-button data-variant="ghost" data-size="sm" data-action="clearIngestion">
Cancel
</ds-button>
</div>
`;
}
}
// Show suggestions
if (suggestions?.length > 0) {
return `
<div class="mt-4 p-3 rounded" style="background: var(--muted)">
<ul class="text-sm text-muted">
${suggestions.map(s => `<li class="mb-1">${s}</li>`).join('')}
</ul>
</div>
`;
}
return '';
}
// === Event Handlers ===
attachEventHandlers() {
// Use event delegation for all [data-action] buttons
// Only attach the listener once to prevent accumulation
if (!this.listeners.hasActionListener) {
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
e.preventDefault();
e.stopPropagation();
const action = btn.dataset.action;
const component = btn.dataset.component;
switch (action) {
case 'extractTokens': this.extractTokens(); break;
case 'extractComponents': this.extractComponents(); break;
case 'syncTokens': this.syncTokens(); break;
case 'runVisualDiff': this.runVisualDiff(); break;
case 'validateComponents': this.validateComponents(); break;
case 'runDiscovery': this.runDiscovery(true); break;
case 'generateCode':
if (component) this.generateCode(component);
break;
case 'saveFigmaKey':
const input = document.getElementById('figma-file-key');
if (input) {
this.store.set({ figmaFileKey: input.value });
this.store.notify('Figma connection saved', 'success');
}
break;
case 'saveProjectDescription':
const descInput = document.getElementById('project-description');
const selectedProject = this.store.get('selectedProject');
if (descInput && selectedProject) {
this.updateProjectDescription(selectedProject.id, descInput.value);
}
break;
// Settings actions
case 'saveFigmaToken':
this.saveFigmaToken();
break;
case 'testFigmaConnection':
this.testFigmaConnection();
break;
case 'saveDssHost':
this.saveDssHost();
break;
case 'setMode':
const mode = btn.dataset.mode;
if (mode) this.setServerMode(mode);
break;
case 'toggleFeature':
const feature = btn.dataset.feature;
if (feature) this.toggleFeature(feature);
break;
case 'refreshServices':
this.loadServices();
break;
case 'initStorybook':
this.initStorybook();
break;
case 'clearStorybook':
this.clearStorybook();
break;
case 'resetDSS':
this.resetDSS();
break;
// Ingestion actions
case 'parseIngestion':
this.parseIngestionPrompt();
break;
case 'browseDesignSystems':
this.browseDesignSystems();
break;
case 'closeBrowse':
this.store.set({ browsingDesignSystems: false });
this.render();
break;
case 'selectDesignSystem':
const systemId = btn.dataset.systemId;
if (systemId) this.selectDesignSystem(systemId);
break;
case 'confirmIngestion':
const confirmSystemId = btn.dataset.systemId;
if (confirmSystemId) this.confirmIngestion(confirmSystemId);
break;
case 'showAlternatives':
const altSystemId = btn.dataset.systemId;
if (altSystemId) this.showIngestionAlternatives(altSystemId);
break;
case 'clearIngestion':
this.store.set({ ingestionResult: null, browsingDesignSystems: false });
this.render();
break;
case 'navigate-quick-wins':
window.location.hash = 'quick-wins';
break;
case 'searchNpm':
const npmQuery = btn.dataset.query;
if (npmQuery) this.searchNpmPackages(npmQuery);
break;
case 'selectIngestionMethod':
const methodType = btn.dataset.method;
if (methodType) this.selectIngestionMethod(methodType);
break;
// Project actions
case 'toggleCreateProject':
this.store.set({ showCreateProjectForm: !this.store.get('showCreateProjectForm') });
this.render();
break;
case 'submitCreateProject':
const nameInput = document.getElementById('new-project-name');
if (nameInput && nameInput.value) {
this.createProject(
nameInput.value,
'', // description
'' // figma key
);
} else {
this.store.notify('Please enter a project name', 'warning');
}
break;
case 'selectProject':
const selectProjectId = btn.dataset.projectId;
if (selectProjectId) this.selectProject(selectProjectId);
break;
case 'syncProject':
const syncProjectId = btn.dataset.projectId;
const syncFigmaKey = btn.dataset.figmaKey;
if (syncProjectId) this.syncProjectTokens(syncProjectId, syncFigmaKey);
break;
case 'deleteProject':
const deleteProjectId = btn.dataset.projectId;
if (deleteProjectId) this.deleteProject(deleteProjectId);
break;
// Token export format
case 'setExportFormat':
const format = btn.dataset.format;
if (format) {
this.store.set({ tokenExportFormat: format });
this.render();
}
break;
// Component actions
case 'viewComponentCode':
const compName = btn.dataset.component;
const compFramework = btn.dataset.framework || 'react';
if (compName) this.showComponentCode(compName, compFramework);
break;
case 'setFramework':
const fw = btn.dataset.framework;
if (fw) {
this.store.set({ componentFramework: fw });
this.render();
}
break;
case 'closeGeneratedCode':
this.store.set({ generatedCode: null });
this.render();
break;
case 'copyCode':
const code = this.store.get('generatedCode')?.code;
if (code) {
navigator.clipboard.writeText(code);
this.store.notify('Code copied to clipboard', 'success');
}
break;
// New services and quick wins actions
case 'executeTool':
const toolName = btn.dataset.tool;
if (toolName) this.executeToolWithParams(toolName);
break;
case 'loadQuickWins':
this.loadQuickWins();
break;
case 'investigateWin':
const winId = btn.dataset.winId;
if (winId) this.investigateWin(winId);
break;
case 'markDone':
const doneWinId = btn.dataset.winId;
if (doneWinId) this.markWinDone(doneWinId);
break;
// Chat actions
case 'sendChatMessage':
this.sendChatMessage({ target: btn });
break;
case 'clearChat':
claudeService.clearHistory();
this.render();
break;
case 'exportChat':
claudeService.exportConversation();
break;
// Team Dashboard actions
case 'sync-figma-file':
const fileId = btn.dataset.fileId;
if (fileId) this.syncFigmaFile(fileId);
break;
case 'delete-figma-file':
const deleteFileId = btn.dataset.fileId;
if (deleteFileId && confirm('Delete this Figma file?')) {
this.deleteFigmaFile(deleteFileId);
}
break;
// Plugin actions
case 'togglePlugin':
const pluginId = btn.dataset.plugin;
if (pluginId) this.togglePlugin(pluginId);
break;
case 'setPluginSetting':
const settingPluginId = btn.dataset.plugin;
const settingKey = btn.dataset.key;
const settingType = btn.dataset.type;
if (settingPluginId && settingKey) {
let settingValue;
if (settingType === 'boolean') {
const current = pluginService.getPluginSettings(settingPluginId)[settingKey];
settingValue = !current;
} else if (btn.tagName === 'SELECT') {
settingValue = btn.value;
}
this.setPluginSetting(settingPluginId, settingKey, settingValue);
}
break;
// Theme actions
case 'toggle-theme':
themeManager.toggle();
this.render();
break;
// Audit actions
case 'load-audit':
this.loadAuditLog();
break;
case 'clear-audit-filters':
this.clearAuditFilters();
break;
case 'export-audit':
const exportFormat = btn.dataset.format || 'json';
this.exportAuditLog(exportFormat);
break;
case 'prev-audit-page':
this.prevAuditPage();
break;
case 'next-audit-page':
this.nextAuditPage();
break;
case 'show-audit-details':
const auditId = btn.dataset.auditId;
if (auditId) this.showAuditDetails(parseInt(auditId));
break;
}
});
this.listeners.hasActionListener = true;
}
// Chat form submission
const chatForm = document.getElementById('chatForm');
if (chatForm) {
chatForm.addEventListener('submit', (e) => {
e.preventDefault();
this.sendChatMessage(e);
});
}
// Team Dashboard form submissions
const addFigmaFileForm = document.getElementById('add-figma-file-form');
if (addFigmaFileForm) {
addFigmaFileForm.addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(e.target);
this.addFigmaFile({
file_name: formData.get('file_name'),
figma_url: formData.get('figma_url'),
file_key: formData.get('file_key')
});
});
}
const addESREForm = document.getElementById('add-esre-form');
if (addESREForm) {
addESREForm.addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(e.target);
this.addESREDefinition({
name: formData.get('name'),
definition_text: formData.get('definition_text'),
component_name: formData.get('component_name') || null,
expected_value: formData.get('expected_value') || null
});
});
}
// Search and filter handlers for services page
const toolSearch = document.getElementById('toolSearch');
if (toolSearch) {
toolSearch.addEventListener('input', (e) => {
this.filterTools(e.target.value, document.getElementById('categoryFilter')?.value || '');
});
}
const categoryFilter = document.getElementById('categoryFilter');
if (categoryFilter) {
categoryFilter.addEventListener('change', (e) => {
this.filterTools(document.getElementById('toolSearch')?.value || '', e.target.value);
});
}
// Docs navigation
document.querySelectorAll('.docs-nav__link[data-doc]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const docId = e.currentTarget.dataset.doc;
const content = document.getElementById('docs-content');
if (content && docId) {
setSafeHtml(content, this.getDocContent(docId));
document.querySelectorAll('.docs-nav__link').forEach(l => l.classList.remove('active'));
e.currentTarget.classList.add('active');
}
});
});
// Setup keyboard shortcuts
this.setupKeyboardHandlers();
}
setupKeyboardHandlers() {
// Handle global keyboard shortcuts
if (!this.listeners.hasKeyboardListener) {
this.listeners.keyboardHandler = (e) => {
// Escape key: Close sidebars and modals
if (e.key === 'Escape') {
// Close AI sidebar if visible
const aiSidebar = document.getElementById('ai-sidebar');
const toggleBtn = document.getElementById('sidebar-toggle');
if (aiSidebar && !aiSidebar.classList.contains('hidden')) {
if (toggleBtn) {
toggleBtn.click();
// Update ARIA state
setTimeout(() => {
const isExpanded = !aiSidebar.classList.contains('hidden');
toggleBtn.setAttribute('aria-expanded', isExpanded);
}, 0);
}
}
// Close any open details/summary elements
document.querySelectorAll('details[open]').forEach(details => {
details.open = false;
});
}
// Alt+N: Navigate to next page
if (e.altKey && e.key === 'n') {
e.preventDefault();
const navItems = Array.from(document.querySelectorAll('.nav-item'));
const currentPage = this.store.get('currentPage');
const currentIndex = navItems.findIndex(item => item.dataset.page === currentPage);
if (currentIndex >= 0 && currentIndex < navItems.length - 1) {
navItems[currentIndex + 1].focus();
navItems[currentIndex + 1].click();
}
}
// Alt+P: Navigate to previous page
if (e.altKey && e.key === 'p') {
e.preventDefault();
const navItems = Array.from(document.querySelectorAll('.nav-item'));
const currentPage = this.store.get('currentPage');
const currentIndex = navItems.findIndex(item => item.dataset.page === currentPage);
if (currentIndex > 0) {
navItems[currentIndex - 1].focus();
navItems[currentIndex - 1].click();
}
}
};
document.addEventListener('keydown', this.listeners.keyboardHandler);
this.listeners.hasKeyboardListener = true;
}
}
// === Settings Actions ===
async saveFigmaToken() {
const input = document.getElementById('figma-token-input');
if (!input || !input.value || input.value.includes('••••')) {
this.store.notify('Please enter a valid Figma token', 'warning');
return;
}
this.store.setLoading('saveFigmaToken', true);
try {
const api = (await import('./api.js')).default;
await api.setFigmaToken(input.value);
this.store.set({ figmaConfig: { configured: true } });
this.store.notify('Figma token saved', 'success');
this.render();
} catch (error) {
this.store.notify(`Failed to save token: ${error.message}`, 'error');
} finally {
this.store.setLoading('saveFigmaToken', false);
}
}
async testFigmaConnection() {
this.store.setLoading('testFigma', true);
try {
const api = (await import('./api.js')).default;
const result = await api.testFigmaConnection();
this.store.set({ figmaTestResult: result });
if (result.success) {
this.store.notify('Figma connection successful!', 'success');
} else {
this.store.notify(`Connection failed: ${result.error}`, 'error');
}
this.render();
} catch (error) {
this.store.set({ figmaTestResult: { success: false, error: error.message } });
this.store.notify(`Test failed: ${error.message}`, 'error');
this.render();
} finally {
this.store.setLoading('testFigma', false);
}
}
async saveDssHost() {
const input = document.getElementById('dss-host-input');
if (!input) {
notifyError('DSS host input not found', ErrorCode.SYSTEM_UNEXPECTED);
return;
}
const newHost = input.value.trim() || 'localhost';
this.store.setLoading('saveDssHost', true);
try {
// Validate hostname format (alphanumeric, dots, hyphens)
if (!/^[a-zA-Z0-9.-]+$/.test(newHost)) {
notifyError('Invalid hostname format', ErrorCode.VALIDATION_INVALID_FORMAT, {
host: newHost
});
this.store.setLoading('saveDssHost', false);
return;
}
// Save to store (will auto-persist via subscription)
this.store.set({ dssHost: newHost });
// Update the link immediately
this.updateStorybookLink();
// Notify success
notifySuccess('Host configuration saved successfully!', ErrorCode.SUCCESS_UPDATED, {
host: newHost,
storybookUrl: `${window.location.protocol}//${newHost}:6006`
});
this.render();
} catch (error) {
handleError(error, {
operation: 'save DSS host',
host: newHost
});
} finally {
this.store.setLoading('saveDssHost', false);
}
}
async resetDSS() {
// Confirm with user
const confirmed = window.confirm(
'⚠️ WARNING: This will delete all user-created themes, projects, cached data, and databases.\n\n' +
'The DSS structure and default themes will be preserved.\n\n' +
'Type "RESET" in the next prompt to confirm.'
);
if (!confirmed) {
return;
}
const confirmText = window.prompt('Type RESET (all caps) to confirm:');
if (confirmText !== 'RESET') {
this.store.notify('Reset cancelled', 'info');
return;
}
this.store.setLoading('resetDSS', true);
try {
this.store.notify('Resetting DSS... This may take a moment.', 'info');
// Call Python reset command via shell
const response = await fetch('/api/system/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ confirm: 'RESET' })
});
if (!response.ok) {
throw new Error('Reset command failed');
}
const result = await response.json();
this.store.notify('✅ DSS has been reset to fresh state!', 'success');
// Reload after a short delay
setTimeout(() => {
window.location.reload();
}, 2000);
} catch (error) {
this.store.notify(`Reset failed: ${error.message}`, 'error');
} finally {
this.store.setLoading('resetDSS', false);
}
}
async setServerMode(mode) {
try {
const api = (await import('./api.js')).default;
await api.setMode(mode);
const config = this.store.get('runtimeConfig') || {};
config.mode = mode;
this.store.set({ runtimeConfig: config });
this.store.notify(`Server mode set to ${mode}`, 'success');
this.render();
} catch (error) {
this.store.notify(`Failed to set mode: ${error.message}`, 'error');
}
}
async toggleFeature(featureId) {
const config = this.store.get('runtimeConfig') || {};
const features = config.features || {};
const newValue = features[featureId] === false;
try {
const api = (await import('./api.js')).default;
await api.updateConfig({ features: { [featureId]: newValue } });
features[featureId] = newValue;
config.features = features;
this.store.set({ runtimeConfig: config });
this.store.notify(`${featureId} ${newValue ? 'enabled' : 'disabled'}`, 'success');
this.render();
} catch (error) {
this.store.notify(`Failed to toggle feature: ${error.message}`, 'error');
}
}
async togglePlugin(pluginId) {
try {
await pluginService.toggle(pluginId);
const plugin = pluginService.get(pluginId);
this.store.notify(`${plugin.name} ${plugin.enabled ? 'enabled' : 'disabled'}`, 'success');
this.render();
} catch (error) {
this.store.notify(`Failed to toggle plugin: ${error.message}`, 'error');
}
}
setPluginSetting(pluginId, key, value) {
pluginService.setPluginSetting(pluginId, key, value);
this.store.notify('Setting updated', 'success');
this.render();
}
async loadServices() {
try {
const api = (await import('./api.js')).default;
const services = await api.getServices();
this.store.set({ services });
this.render();
} catch (error) {
console.error('Failed to load services:', error);
}
}
async initStorybook() {
try {
this.store.notify('Initializing Storybook...', 'info');
const api = (await import('./api.js')).default;
const result = await api.post('/storybook/init', {});
if (result.success) {
this.store.notify(result.message || 'Storybook initialized', 'success');
} else {
this.store.notify('Failed to initialize Storybook', 'error');
}
} catch (error) {
console.error('Failed to initialize Storybook:', error);
this.store.notify(`Failed to initialize Storybook: ${error.message}`, 'error');
}
}
async clearStorybook() {
try {
this.store.notify('Clearing Storybook stories...', 'info');
const api = (await import('./api.js')).default;
const result = await api.delete('/storybook/stories');
if (result.success) {
this.store.notify(result.message || 'Stories cleared', 'success');
} else {
this.store.notify('Failed to clear stories', 'error');
}
} catch (error) {
console.error('Failed to clear Storybook:', error);
this.store.notify(`Failed to clear stories: ${error.message}`, 'error');
}
}
// === Design System Ingestion ===
async parseIngestionPrompt() {
const input = document.getElementById('ingest-prompt');
if (!input || !input.value.trim()) {
this.store.notify('Please enter a design system name or prompt', 'warning');
return;
}
try {
this.store.setLoading('ingestion', true);
this.store.notify('Parsing ingestion prompt...', 'info');
const api = (await import('./api.js')).default;
const result = await api.post('/ingest/parse', {
prompt: input.value.trim(),
project_id: this.store.get('currentProject')
});
this.store.set({ ingestionResult: result });
this.render();
// Show helpful message
if (result.sources?.length > 0 && result.sources[0].matched_system) {
this.store.notify(`Found: ${result.sources[0].matched_system.name}`, 'success');
} else if (result.next_steps?.length > 0) {
this.store.notify(result.next_steps[0].message || 'Processing...', 'info');
}
} catch (error) {
console.error('Ingestion parse failed:', error);
this.store.notify(`Failed to parse: ${error.message}`, 'error');
} finally {
this.store.setLoading('ingestion', false);
}
}
async browseDesignSystems() {
try {
this.store.notify('Loading design systems...', 'info');
const api = (await import('./api.js')).default;
const result = await api.get('/ingest/systems');
this.store.set({
designSystems: result.systems || [],
browsingDesignSystems: true,
ingestionResult: null
});
this.render();
} catch (error) {
console.error('Failed to load design systems:', error);
this.store.notify(`Failed to load: ${error.message}`, 'error');
}
}
async selectDesignSystem(systemId) {
try {
const api = (await import('./api.js')).default;
const result = await api.get(`/ingest/systems/${systemId}`);
this.store.set({
ingestionResult: {
sources: [{ matched_system: result.system }],
alternatives: result.alternatives
},
browsingDesignSystems: false
});
this.render();
} catch (error) {
console.error('Failed to select design system:', error);
this.store.notify(`Failed: ${error.message}`, 'error');
}
}
async confirmIngestion(systemId) {
try {
this.store.setLoading('ingestion', true);
this.store.notify('Starting ingestion...', 'info');
const api = (await import('./api.js')).default;
const result = await api.post('/ingest/confirm', {
system_id: systemId,
method: 'npm' // Default to npm
});
if (result.success) {
this.store.notify(`Queued: ${result.message}`, 'success');
// Show next steps
if (result.next_steps?.length > 0) {
setTimeout(() => {
this.store.notify(`Next: ${result.next_steps.join(' → ')}`, 'info');
}, 1500);
}
// Clear ingestion UI
this.store.set({ ingestionResult: null });
this.render();
} else {
this.store.notify('Ingestion failed', 'error');
}
} catch (error) {
console.error('Ingestion failed:', error);
this.store.notify(`Failed: ${error.message}`, 'error');
} finally {
this.store.setLoading('ingestion', false);
}
}
async showIngestionAlternatives(systemId) {
try {
const api = (await import('./api.js')).default;
const result = await api.get(`/ingest/alternatives?system_id=${systemId}`);
// Update the ingestion result to show alternatives
const currentResult = this.store.get('ingestionResult') || {};
this.store.set({
ingestionResult: {
...currentResult,
next_steps: [{
action: 'request_source',
message: 'Choose an ingestion method:',
alternatives: result.alternatives || []
}]
}
});
this.render();
} catch (error) {
console.error('Failed to get alternatives:', error);
this.store.notify(`Failed: ${error.message}`, 'error');
}
}
async searchNpmPackages(query) {
try {
this.store.notify(`Searching npm for "${query}"...`, 'info');
const api = (await import('./api.js')).default;
const result = await api.get(`/ingest/npm/search?query=${encodeURIComponent(query)}`);
if (result.packages?.length > 0) {
// Show npm results as design systems to choose from
this.store.set({
designSystems: result.packages.map(pkg => ({
id: pkg.name,
name: pkg.name,
description: pkg.description,
npm_packages: [pkg.name],
category: pkg.is_design_system ? 'npm-design-system' : 'npm-package',
homepage: pkg.homepage
})),
browsingDesignSystems: true,
ingestionResult: null
});
this.store.notify(`Found ${result.packages.length} packages`, 'success');
} else {
this.store.notify('No packages found on npm', 'warning');
}
this.render();
} catch (error) {
console.error('npm search failed:', error);
this.store.notify(`Search failed: ${error.message}`, 'error');
}
}
selectIngestionMethod(methodType) {
// Show appropriate UI for the selected method
const prompts = {
figma_url: 'Enter Figma file URL:',
css_url: 'Enter CSS file URL:',
github_url: 'Enter GitHub repository URL:',
image_url: 'Upload or enter image URL:',
text_description: 'Describe your design tokens:'
};
const prompt = prompts[methodType] || 'Enter source:';
const value = window.prompt(prompt);
if (value) {
// Store the method and source for later use
this.store.set({
ingestionMethod: methodType,
ingestionSource: value
});
this.store.notify(`Source saved. Click "Ingest" to proceed.`, 'success');
}
}
async loadConfig() {
try {
const api = (await import('./api.js')).default;
const [configData, figmaConfig, services] = await Promise.all([
api.getConfig(),
api.getFigmaConfig(),
api.getServices()
]);
this.store.set({
runtimeConfig: configData.config || {},
figmaConfig: figmaConfig || {},
services: services || {}
});
} catch (error) {
console.error('Failed to load config:', error);
}
}
// === Project Management ===
async loadProjects() {
try {
const api = (await import('./api.js')).default;
const projects = await api.getProjects();
this.store.setProjects(Array.isArray(projects) ? projects : []);
// Render project selector after projects are loaded
this.renderProjectSelector();
} catch (error) {
console.error('Failed to load projects:', error);
this.store.setProjects([]);
this.renderProjectSelector();
}
}
async loadDashboardData(projectId) {
if (!projectId) return;
try {
logger.info('App', 'Loading dashboard data', { projectId });
const dashboardData = await DashboardService.getDashboardSummary(projectId);
this.store.set({ dashboardData });
this.render(); // Re-render to show updated data
} catch (error) {
logger.error('App', 'Failed to load dashboard data', error);
// Set empty dashboard data on error
this.store.set({
dashboardData: {
ux: { figma_files_count: 0, figma_files: [] },
ui: { token_drift: { total: 0, by_severity: {} }, code_metrics: {} },
qa: { esre_count: 0, test_summary: {} }
}
});
}
}
renderProjectSelector() {
const container = document.getElementById('project-selector-container');
if (!container) return;
const projects = this.store.get('projects') || [];
const currentProject = this.store.get('currentProject') || (projects.length > 0 ? projects[0].id : null);
// Restore from localStorage if available
const savedProject = localStorage.getItem('dss_current_project');
if (savedProject && projects.find(p => p.id === savedProject)) {
this.store.set({ currentProject: savedProject });
} else if (currentProject) {
this.store.set({ currentProject });
}
const html = `
<div class="project-selector">
<svg class="project-selector__icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M3 3h18v18H3z"/>
<path d="M21 9H3"/>
<path d="M9 21V9"/>
</svg>
<span class="project-selector__label">Project:</span>
<select class="project-selector__select" id="project-select">
${projects.length === 0
? '<option value="">No projects</option>'
: projects.map(p => `
<option value="${escapeHtml(p.id)}" ${p.id === (currentProject || savedProject) ? 'selected' : ''}>
${escapeHtml(p.name)}
</option>
`).join('')}
</select>
</div>
`;
setSafeHtml(container, html);
// Attach event listener
const select = document.getElementById('project-select');
if (select) {
select.addEventListener('change', (e) => {
const projectId = e.target.value;
this.handleProjectChange(projectId);
});
}
}
handleProjectChange(projectId) {
logger.info('App', 'Project context changed', { projectId });
// Save to store and localStorage
this.store.set({ currentProject: projectId });
localStorage.setItem('dss_current_project', projectId);
// Get project details
const projects = this.store.get('projects') || [];
const project = projects.find(p => p.id === projectId);
// Trigger refresh
this.render();
logger.info('App', 'Project context set', { project: project?.name });
}
// === Audit Log ===
renderAuditLog() {
// Load audit log data after render
setTimeout(() => this.loadAuditLog(), 100);
return `
<div class="page-header">
<h1>Audit Log</h1>
<p class="text-muted">History of all user actions and system events</p>
</div>
<!-- Filters -->
<div class="mt-6">
<ds-card>
<ds-card-header>
<ds-card-title>Filters</ds-card-title>
</ds-card-header>
<ds-card-content>
<div class="grid grid-cols-4 gap-4">
<div>
<label class="text-sm font-medium">Category</label>
<select id="audit-category-filter" class="w-full mt-2 p-2 rounded border">
<option value="">All Categories</option>
<option value="design_system">Design System</option>
<option value="code">Code</option>
<option value="configuration">Configuration</option>
<option value="project">Project</option>
<option value="team">Team</option>
<option value="storybook">Storybook</option>
</select>
</div>
<div>
<label class="text-sm font-medium">Severity</label>
<select id="audit-severity-filter" class="w-full mt-2 p-2 rounded border">
<option value="">All Severities</option>
<option value="info">Info</option>
<option value="warning">Warning</option>
<option value="critical">Critical</option>
</select>
</div>
<div>
<label class="text-sm font-medium">Start Date</label>
<input type="date" id="audit-start-date" class="w-full mt-2 p-2 rounded border">
</div>
<div>
<label class="text-sm font-medium">End Date</label>
<input type="date" id="audit-end-date" class="w-full mt-2 p-2 rounded border">
</div>
</div>
<div class="flex gap-3 mt-4">
<ds-button data-variant="primary" data-action="load-audit">
Apply Filters
</ds-button>
<ds-button data-variant="outline" data-action="clear-audit-filters">
Clear Filters
</ds-button>
<ds-button data-variant="outline" data-action="export-audit" data-format="json">
📥 Export JSON
</ds-button>
<ds-button data-variant="outline" data-action="export-audit" data-format="csv">
📥 Export CSV
</ds-button>
</div>
</ds-card-content>
</ds-card>
</div>
<!-- Audit Log Table -->
<div class="mt-6">
<ds-card>
<ds-card-header>
<ds-card-title>Activity History</ds-card-title>
<ds-card-description>
<span id="audit-total-count">Loading...</span>
</ds-card-description>
</ds-card-header>
<ds-card-content>
<div id="audit-log-content">
<div class="text-center py-8 text-muted">
Loading audit log...
</div>
</div>
<!-- Pagination -->
<div class="flex justify-between items-center mt-6" id="audit-pagination" style="display: none;">
<div class="text-sm text-muted">
Showing <span id="audit-showing"></span> of <span id="audit-total"></span> entries
</div>
<div class="flex gap-2">
<ds-button data-variant="outline" data-size="sm" id="audit-prev-btn" data-action="prev-audit-page">
Previous
</ds-button>
<ds-button data-variant="outline" data-size="sm" id="audit-next-btn" data-action="next-audit-page">
Next
</ds-button>
</div>
</div>
</ds-card-content>
</ds-card>
</div>
`;
}
async loadAuditLog() {
const auditService = (await import('../services/audit-service.js')).default;
const content = document.getElementById('audit-log-content');
if (!content) return;
// Get filter values
const filters = {
category: document.getElementById('audit-category-filter')?.value || '',
severity: document.getElementById('audit-severity-filter')?.value || '',
start_date: document.getElementById('audit-start-date')?.value || '',
end_date: document.getElementById('audit-end-date')?.value || '',
limit: 50,
offset: this.auditLogOffset || 0
};
// Add current project filter if set
const currentProject = this.store.get('currentProject');
if (currentProject) {
filters.project_id = currentProject;
}
try {
const result = await auditService.getAuditLog(filters);
// Update total count
const totalCount = document.getElementById('audit-total-count');
if (totalCount) {
totalCount.textContent = `${result.total} total events`;
}
// Render table
if (result.activities && result.activities.length > 0) {
const html = `
<table class="w-full">
<thead>
<tr class="border-b">
<th class="text-left p-3 text-sm font-medium">Time</th>
<th class="text-left p-3 text-sm font-medium">User</th>
<th class="text-left p-3 text-sm font-medium">Action</th>
<th class="text-left p-3 text-sm font-medium">Category</th>
<th class="text-left p-3 text-sm font-medium">Description</th>
<th class="text-left p-3 text-sm font-medium">Severity</th>
</tr>
</thead>
<tbody>
${result.activities.map(activity => `
<tr class="border-b hover:bg-muted/50 cursor-pointer" data-action="show-audit-details" data-audit-id="${activity.id}">
<td class="p-3 text-sm text-muted">
${escapeHtml(auditService.formatTimestamp(activity.created_at))}
</td>
<td class="p-3 text-sm">
${escapeHtml(activity.user_name || activity.user_id || 'System')}
</td>
<td class="p-3 text-sm font-mono text-xs">
${escapeHtml(activity.action)}
</td>
<td class="p-3 text-sm">
${auditService.getCategoryIcon(activity.category)} ${escapeHtml(activity.category || 'other')}
</td>
<td class="p-3 text-sm">
${escapeHtml(activity.description || '-')}
</td>
<td class="p-3">
<ds-badge data-variant="${escapeHtml(auditService.getSeverityClass(activity.severity))}">
${escapeHtml(activity.severity)}
</ds-badge>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
setSafeHtml(content, html);
} else {
content.innerHTML = `
<div class="text-center py-8 text-muted">
No audit entries found
</div>
`;
}
// Update pagination
const pagination = document.getElementById('audit-pagination');
if (pagination && result.total > 0) {
pagination.style.display = 'flex';
const showing = document.getElementById('audit-showing');
const total = document.getElementById('audit-total');
const prevBtn = document.getElementById('audit-prev-btn');
const nextBtn = document.getElementById('audit-next-btn');
const start = filters.offset + 1;
const end = Math.min(filters.offset + result.activities.length, result.total);
if (showing) showing.textContent = `${start}-${end}`;
if (total) total.textContent = result.total;
if (prevBtn) {
if (filters.offset === 0) {
prevBtn.setAttribute('disabled', '');
} else {
prevBtn.removeAttribute('disabled');
}
}
if (nextBtn) {
if (!result.has_more) {
nextBtn.setAttribute('disabled', '');
} else {
nextBtn.removeAttribute('disabled');
}
}
this.auditLogData = result;
}
} catch (error) {
content.innerHTML = `
<div class="text-center py-8 text-destructive">
Failed to load audit log: ${error.message}
</div>
`;
}
}
clearAuditFilters() {
document.getElementById('audit-category-filter').value = '';
document.getElementById('audit-severity-filter').value = '';
document.getElementById('audit-start-date').value = '';
document.getElementById('audit-end-date').value = '';
this.auditLogOffset = 0;
this.loadAuditLog();
}
nextAuditPage() {
const limit = 50;
this.auditLogOffset = (this.auditLogOffset || 0) + limit;
this.loadAuditLog();
}
prevAuditPage() {
const limit = 50;
this.auditLogOffset = Math.max(0, (this.auditLogOffset || 0) - limit);
this.loadAuditLog();
}
async exportAuditLog(format = 'json') {
const auditService = (await import('../services/audit-service.js')).default;
const filters = {
category: document.getElementById('audit-category-filter')?.value || '',
start_date: document.getElementById('audit-start-date')?.value || '',
end_date: document.getElementById('audit-end-date')?.value || ''
};
// Add current project filter
const currentProject = this.store.get('currentProject');
if (currentProject) {
filters.project_id = currentProject;
}
await auditService.export(filters, format);
this.store.notify(`Audit log export started (${format})`, 'success');
}
showAuditDetails(id) {
// TODO: Show modal with full audit entry details
console.log('Show audit details for:', id);
}
async createProject(name, description, figmaFileKey) {
this.store.setLoading('createProject', true);
try {
const api = (await import('./api.js')).default;
const project = await api.createProject({
name,
description,
figma_file_key: figmaFileKey
});
this.store.addProject(project);
this.store.set({ showCreateProjectForm: false });
this.store.notify(`Project "${name}" created successfully`, 'success');
this.render();
return project;
} catch (error) {
this.store.notify(`Failed to create project: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('createProject', false);
}
}
async updateProjectDescription(projectId, description) {
this.store.setLoading('updateProject', true);
try {
const api = (await import('./api.js')).default;
await api.updateProject(projectId, { description });
// Update project in store
const projects = this.store.get('projects').map(p =>
p.id === projectId ? { ...p, description } : p
);
this.store.setProjects(projects);
// Update selected project
const selectedProject = this.store.get('selectedProject');
if (selectedProject && selectedProject.id === projectId) {
this.store.set({ selectedProject: { ...selectedProject, description } });
}
this.store.notify('Project description updated', 'success');
this.render();
} catch (error) {
this.store.notify(`Failed to update description: ${error.message}`, 'error');
} finally {
this.store.setLoading('updateProject', false);
}
}
async deleteProject(projectId) {
if (!confirm('Are you sure you want to delete this project? This cannot be undone.')) {
return;
}
this.store.setLoading('deleteProject', true);
try {
const api = (await import('./api.js')).default;
await api.deleteProject(projectId);
const projects = this.store.get('projects').filter(p => p.id !== projectId);
this.store.setProjects(projects);
this.store.notify('Project deleted', 'success');
this.render();
} catch (error) {
this.store.notify(`Failed to delete project: ${error.message}`, 'error');
} finally {
this.store.setLoading('deleteProject', false);
}
}
selectProject(projectId) {
const project = this.store.get('projects').find(p => p.id === projectId);
if (project) {
this.store.set({
selectedProject: project,
figmaFileKey: project.figma_file_key
});
this.store.notify(`Selected project: ${project.name}`, 'info');
// Navigate to dashboard with project context
this.navigate('dashboard');
}
}
async syncProjectTokens(projectId, figmaKey) {
if (!figmaKey) {
this.store.notify('No Figma file key configured for this project', 'warning');
return;
}
this.store.set({ figmaFileKey: figmaKey });
await this.extractTokens();
await this.loadProjects(); // Refresh to get updated last_sync
}
// === Notifications ===
renderNotifications(notifications) {
let container = document.getElementById('notifications');
if (!container) {
container = document.createElement('div');
container.id = 'notifications';
container.className = 'notification-container'; // Use CSS class instead of inline styles
document.body.appendChild(container);
}
const html = notifications.map(n => {
const notificationType = escapeHtml(n.type);
const notificationMessage = escapeHtml(n.message);
return `
<div class="notification notification--${notificationType}">
${notificationMessage}
</div>
`;
}).join('');
setSafeHtml(container, html);
}
// === Services Page ===
renderServices() {
const teamContext = this.store.get('teamContext') || localStorage.getItem('dss_team_context') || 'all';
const tools = toolsService.getToolsByTeam(teamContext);
const categories = toolsService.getCategories();
const teamNames = {
'all': 'All Tools',
'ui': 'UI Team',
'ux': 'UX Team',
'qa': 'QA Team'
};
// Group tools by category
const toolsByCategory = {};
tools.forEach(tool => {
if (!toolsByCategory[tool.category]) {
toolsByCategory[tool.category] = [];
}
toolsByCategory[tool.category].push(tool);
});
// Category icons
const categoryIcons = {
'Projects': '<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>',
'Figma': '<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 5.5A3.5 3.5 0 0 1 8.5 2H12v7H8.5A3.5 3.5 0 0 1 5 5.5z"/><path d="M12 2h3.5a3.5 3.5 0 1 1 0 7H12V2z"/><path d="M12 12.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 1 1-7 0z"/><path d="M5 19.5A3.5 3.5 0 0 1 8.5 16H12v3.5a3.5 3.5 0 1 1-7 0z"/><path d="M5 12.5A3.5 3.5 0 0 1 8.5 9H12v7H8.5A3.5 3.5 0 0 1 5 12.5z"/></svg>',
'Ingestion': '<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
'Analysis': '<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0z"/></svg>',
'Storybook': '<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
'Activity': '<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>'
};
logger.info('UI', 'Rendering services page', { toolCount: tools.length, teamContext, categories });
return `
<div class="page-header">
<div class="flex items-center justify-between">
<div>
<h1>Services</h1>
<p class="text-muted">${tools.length} tools available</p>
</div>
<input type="text" id="toolSearch" placeholder="Search..." class="input" style="width: 200px;" />
</div>
</div>
<div id="toolsContainer" class="tools-container">
${categories.map(category => {
const categoryTools = toolsByCategory[category] || [];
if (categoryTools.length === 0) return '';
return `
<div class="tools-category" data-category="${category}">
<div class="tools-category__header">
<span class="tools-category__icon">${categoryIcons[category] || '📦'}</span>
<span class="tools-category__name">${category}</span>
<span class="tools-category__count">${categoryTools.length}</span>
</div>
<div class="tools-category__list">
${categoryTools.map(tool => this.renderToolCard(tool)).join('')}
</div>
</div>
`;
}).join('')}
</div>
<style>
.tools-container { display: flex; flex-direction: column; gap: var(--space-4); }
.tools-category { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
.tools-category__header { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); background: var(--muted); border-bottom: 1px solid var(--border); }
.tools-category__icon { color: var(--primary); display: flex; }
.tools-category__name { font-weight: var(--font-semibold); flex: 1; }
.tools-category__count { font-size: var(--text-xs); color: var(--muted-foreground); background: var(--background); padding: 2px 8px; border-radius: var(--radius-full); }
.tools-category__list { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
.tool-item { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.15s; }
.tool-item:last-child { border-bottom: none; }
.tool-item:hover { background: var(--accent); }
.tool-item__icon { font-size: 1.25rem; width: 28px; text-align: center; }
.tool-item__info { flex: 1; min-width: 0; }
.tool-item__name { font-size: var(--text-sm); font-weight: var(--font-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tool-item__desc { font-size: var(--text-xs); color: var(--muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tool-item__action { opacity: 0; transition: opacity 0.15s; }
.tool-item:hover .tool-item__action { opacity: 1; }
.tool-item__params { font-size: 10px; color: var(--muted-foreground); }
</style>
`;
}
renderToolCard(tool) {
return `
<div class="tool-item" data-tool="${tool.name}" data-category="${tool.category}" data-action="executeTool" title="${tool.description}">
<span class="tool-item__icon">${tool.icon}</span>
<div class="tool-item__info">
<div class="tool-item__name">${tool.name.replace(/_/g, ' ')}</div>
<div class="tool-item__desc">${tool.description}</div>
</div>
${tool.parameters.length > 0 ? `<span class="tool-item__params">${tool.parameters.length}p</span>` : ''}
<svg class="tool-item__action" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</div>
`;
}
// === Quick Wins Page ===
renderQuickWins() {
logger.info('UI', 'Rendering quick wins page');
return `
<div class="page-header">
<h1>⚡ Quick Wins</h1>
<p class="text-muted">Actionable improvements to empower your team</p>
</div>
<div class="card p-4 mb-6">
<div class="flex gap-4">
<input
type="text"
id="projectPath"
placeholder="Project path (default: .)"
value="."
class="input flex-1"
/>
<button
class="btn btn-primary"
data-action="loadQuickWins"
>
🔍 Analyze Project
</button>
</div>
</div>
<div id="quickWinsContainer">
<div class="text-center text-muted py-8">
<p>Click "Analyze Project" to discover quick wins</p>
</div>
</div>
`;
}
async loadQuickWins() {
const path = document.getElementById('projectPath')?.value || '.';
const container = document.getElementById('quickWinsContainer');
if (!container) return;
logger.info('QuickWins', 'Loading quick wins', { path });
container.innerHTML = '<div class="text-center py-8">Analyzing project...</div>';
try {
const result = await toolsService.executeTool('get_quick_wins', { path });
const data = typeof result === 'string' ? JSON.parse(result) : result;
logger.info('QuickWins', 'Quick wins loaded', data);
container.innerHTML = this.renderQuickWinsResults(data);
} catch (error) {
logger.error('QuickWins', 'Failed to load quick wins', error);
container.innerHTML = `
<div class="card p-4 bg-destructive/10">
<p class="text-destructive">Failed to load quick wins: ${error.message}</p>
</div>
`;
}
}
renderQuickWinsResults(data) {
const wins = data.wins || data.quick_wins || [];
if (wins.length === 0) {
return '<div class="card p-4"><p class="text-muted">No quick wins found. Great job!</p></div>';
}
const byPriority = {
CRITICAL: wins.filter(w => w.priority === 'CRITICAL'),
HIGH: wins.filter(w => w.priority === 'HIGH'),
MEDIUM: wins.filter(w => w.priority === 'MEDIUM'),
LOW: wins.filter(w => w.priority === 'LOW')
};
return `
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="card p-4">
<div class="stat">
<div class="stat__label">Critical</div>
<div class="stat__value text-destructive">${byPriority.CRITICAL.length}</div>
</div>
</div>
<div class="card p-4">
<div class="stat">
<div class="stat__label">High</div>
<div class="stat__value text-warning">${byPriority.HIGH.length}</div>
</div>
</div>
<div class="card p-4">
<div class="stat">
<div class="stat__label">Medium</div>
<div class="stat__value">${byPriority.MEDIUM.length}</div>
</div>
</div>
<div class="card p-4">
<div class="stat">
<div class="stat__label">Low</div>
<div class="stat__value text-muted">${byPriority.LOW.length}</div>
</div>
</div>
</div>
<div class="space-y-4">
${wins.map(win => this.renderQuickWinCard(win)).join('')}
</div>
`;
}
renderQuickWinCard(win) {
const priorityColors = {
CRITICAL: 'destructive',
HIGH: 'warning',
MEDIUM: 'primary',
LOW: 'muted'
};
const color = priorityColors[win.priority] || 'muted';
return `
<div class="card p-4">
<div class="flex items-start justify-between mb-3">
<div>
<span class="badge badge-${color}">${win.priority}</span>
<span class="badge badge-outline ml-2">${win.type}</span>
</div>
${win.effort ? `<span class="text-sm text-muted">Effort: ${win.effort}</span>` : ''}
</div>
<h3 class="text-lg font-semibold mb-2">${win.title}</h3>
<p class="text-muted mb-3">${win.description}</p>
${win.file ? `
<div class="text-sm text-muted mb-3">
📄 ${win.file}
</div>
` : ''}
${win.impact ? `
<div class="text-sm mb-3">
<span class="text-muted">Impact:</span> ${win.impact}
</div>
` : ''}
<div class="flex gap-2">
<button class="btn btn-sm btn-primary" data-action="investigateWin" data-win-id="${win.id || Math.random()}">
🔍 Investigate
</button>
<button class="btn btn-sm btn-outline" data-action="markDone" data-win-id="${win.id || Math.random()}">
✓ Mark Done
</button>
</div>
</div>
`;
}
// === Claude Chat Page ===
renderChat() {
const history = claudeService.getHistory();
const selectedModel = this.store.get('selectedAIModel') || 'claude';
logger.info('UI', 'Rendering chat page', { historyLength: history.length, model: selectedModel });
return `
<div class="page-header">
<div class="flex items-center justify-between">
<div>
<h1>💬 AI Assistant</h1>
<p class="text-muted">Ask questions about your design system</p>
</div>
<div class="flex gap-2">
<select
id="aiModelSelect"
class="select select-sm"
data-action="changeAIModel"
style="padding: 4px 8px; border: 1px solid var(--border); border-radius: 4px; background: var(--background);"
>
<option value="claude" ${selectedModel === 'claude' ? 'selected' : ''}>🤖 Claude</option>
<option value="gemini" ${selectedModel === 'gemini' ? 'selected' : ''}>✨ Gemini</option>
</select>
<button
class="btn btn-outline btn-sm"
data-action="exportChat"
>
📥 Export
</button>
<button
class="btn btn-outline btn-sm"
data-action="clearChat"
>
🗑️ Clear
</button>
</div>
</div>
</div>
<div class="card flex flex-col" style="height: calc(100vh - 200px);">
<!-- Chat messages -->
<div id="chatMessages" class="flex-1 overflow-y-auto p-4 space-y-4">
${history.length === 0 ? `
<div class="text-center text-muted py-12">
<p class="text-lg mb-2">👋 Hi! I'm Claude</p>
<p>Ask me anything about your design system, tokens, or code</p>
</div>
` : history.map(msg => this.renderChatMessage(msg)).join('')}
</div>
<!-- Input form -->
<div class="border-t p-4">
<form id="chatForm" class="flex gap-2">
<input
type="text"
id="chatInput"
placeholder="Ask Claude something..."
class="input flex-1"
autocomplete="off"
/>
<button
type="submit"
class="btn btn-primary"
data-action="sendChatMessage"
>
Send
</button>
</form>
</div>
</div>
`;
}
renderChatMessage(msg) {
const isUser = msg.role === 'user';
const bgColor = isUser ? 'bg-primary/10' : 'bg-card';
const align = isUser ? 'ml-auto' : '';
const icon = isUser ? '👤' : '🤖';
return `
<div class="flex ${isUser ? 'justify-end' : 'justify-start'}">
<div class="${bgColor} rounded-lg p-4 max-w-[80%] ${align}">
<div class="flex items-center gap-2 mb-2">
<span>${icon}</span>
<span class="text-sm font-medium">${isUser ? 'You' : 'Claude'}</span>
<span class="text-xs text-muted">${this.formatTime(msg.timestamp)}</span>
</div>
<div class="text-sm whitespace-pre-wrap">${this.escapeHtml(msg.content)}</div>
</div>
</div>
`;
}
async sendChatMessage(e) {
if (e) e.preventDefault();
const input = document.getElementById('chatInput');
const message = input?.value.trim();
if (!message) return;
logger.info('Chat', 'Sending message', { message });
// Clear input
input.value = '';
// Add user message to UI immediately
const messagesContainer = document.getElementById('chatMessages');
if (messagesContainer) {
messagesContainer.innerHTML += this.renderChatMessage({
role: 'user',
content: message,
timestamp: new Date().toISOString()
});
// Add loading indicator
messagesContainer.innerHTML += `
<div id="chatLoading" class="flex justify-start">
<div class="bg-card rounded-lg p-4">
<div class="flex items-center gap-2">
<span>🤖</span>
<span class="text-sm">Claude is typing...</span>
</div>
</div>
</div>
`;
// Scroll to bottom
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
try {
const response = await claudeService.chat(message, {
project: this.store.get('currentProject'),
page: 'chat'
});
// Remove loading indicator
document.getElementById('chatLoading')?.remove();
// Add Claude's response
if (messagesContainer) {
messagesContainer.innerHTML += this.renderChatMessage({
role: 'assistant',
content: response,
timestamp: new Date().toISOString()
});
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
} catch (error) {
logger.error('Chat', 'Failed to send message', error);
document.getElementById('chatLoading')?.remove();
if (messagesContainer) {
messagesContainer.innerHTML += `
<div class="flex justify-start">
<div class="bg-destructive/10 rounded-lg p-4">
<div class="text-destructive text-sm">
❌ Failed to get response: ${error.message}
</div>
</div>
</div>
`;
}
this.store.notify(`Chat error: ${error.message}`, 'error');
}
}
// === Tool Execution Actions ===
async executeToolWithParams(toolName) {
logger.info('ToolExecution', `Preparing to execute ${toolName}`);
const tool = toolsService.toolsMetadata[toolName];
if (!tool) {
logger.error('ToolExecution', `Tool not found: ${toolName}`);
return;
}
// For simple tools with no required params, execute directly
if (tool.parameters.length === 0) {
try {
this.store.notify(`Executing ${toolName}...`, 'info');
const result = await toolsService.executeTool(toolName, {});
logger.info('ToolExecution', `Tool ${toolName} completed`, result);
this.store.notify(`${toolName} completed successfully`, 'success');
this.render();
} catch (error) {
logger.error('ToolExecution', `Tool ${toolName} failed`, error);
this.store.notify(`${toolName} failed: ${error.message}`, 'error');
}
return;
}
// TODO: For tools with parameters, show a modal to collect params
this.store.notify(`${toolName} requires parameters. Parameter input UI coming soon.`, 'info');
}
filterTools(searchTerm, category) {
const cards = document.querySelectorAll('.tool-card');
const search = searchTerm.toLowerCase();
cards.forEach(card => {
const toolName = card.dataset.tool?.toLowerCase() || '';
const toolCategory = card.dataset.category || '';
const matchesSearch = !search || toolName.includes(search);
const matchesCategory = !category || toolCategory === category;
card.style.display = (matchesSearch && matchesCategory) ? 'block' : 'none';
});
}
investigateWin(winId) {
logger.info('QuickWins', `Investigating win: ${winId}`);
this.store.notify('Investigation feature coming soon', 'info');
}
markWinDone(winId) {
logger.info('QuickWins', `Marking win as done: ${winId}`);
this.store.notify('Win marked as done', 'success');
// TODO: Persist to backend
}
// === Team Dashboard Actions ===
async addFigmaFile(figmaData) {
const projectId = this.store.get('currentProject');
if (!projectId) {
this.store.notify('Please select a project first', 'warning');
return;
}
try {
const result = await DashboardService.addFigmaFile(projectId, figmaData);
this.store.notify('Figma file added successfully', 'success');
await this.loadDashboardData(projectId);
} catch (error) {
this.store.notify(`Failed to add Figma file: ${error.message}`, 'error');
}
}
async deleteFigmaFile(fileId) {
const projectId = this.store.get('currentProject');
if (!projectId) return;
try {
await DashboardService.deleteFigmaFile(projectId, fileId);
this.store.notify('Figma file deleted', 'success');
await this.loadDashboardData(projectId);
} catch (error) {
this.store.notify(`Failed to delete Figma file: ${error.message}`, 'error');
}
}
async syncFigmaFile(fileId) {
const projectId = this.store.get('currentProject');
if (!projectId) return;
try {
await DashboardService.updateFigmaFileSync(projectId, fileId, 'syncing');
this.store.notify('Syncing Figma file...', 'info');
// TODO: Implement actual sync logic
setTimeout(async () => {
await DashboardService.updateFigmaFileSync(projectId, fileId, 'success');
this.store.notify('Figma file synced successfully', 'success');
await this.loadDashboardData(projectId);
}, 2000);
} catch (error) {
this.store.notify(`Failed to sync Figma file: ${error.message}`, 'error');
}
}
async addESREDefinition(esreData) {
const projectId = this.store.get('currentProject');
if (!projectId) {
this.store.notify('Please select a project first', 'warning');
return;
}
try {
const result = await DashboardService.createESREDefinition(projectId, esreData);
this.store.notify('ESRE definition added successfully', 'success');
await this.loadDashboardData(projectId);
} catch (error) {
this.store.notify(`Failed to add ESRE definition: ${error.message}`, 'error');
}
}
// === Utilities ===
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
return date.toLocaleDateString();
}
/**
* Update Storybook link with configured URL
* Called on init and when settings are saved
*/
updateStorybookLink() {
const link = document.getElementById('storybook-link');
if (!link) return;
try {
// Get Storybook URL from server config (loaded from /api/config)
const storybookUrl = getStorybookUrl();
link.href = storybookUrl;
link.style.pointerEvents = 'auto';
link.style.opacity = '1';
link.title = `Open Storybook at ${storybookUrl}`;
} catch (error) {
// Config not loaded yet or error occurred
link.style.pointerEvents = 'none';
link.style.opacity = '0.5';
link.removeAttribute('href');
link.title = 'Storybook URL not available';
logger.warn('App', 'Could not update Storybook link', { error: error.message });
}
}
}
// Global instance
const app = new App();
export default app;