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
4351 lines
155 KiB
JavaScript
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;
|