diff --git a/admin-ui/AI-REFERENCE.md b/admin-ui/AI-REFERENCE.md new file mode 100644 index 0000000..d6884f8 --- /dev/null +++ b/admin-ui/AI-REFERENCE.md @@ -0,0 +1,339 @@ +# DSS Admin UI - AI Reference Documentation + +## Overview +The DSS Admin UI is a Preact + Signals application that provides a team-centric dashboard for managing design system operations. It connects to the DSS backend API (FastAPI server running on port 8002). + +## Technology Stack +- **Framework**: Preact 10.x (~3KB) +- **State Management**: @preact/signals (~2KB) +- **Build**: Vite 5.x +- **Language**: TypeScript +- **Routing**: Hash-based (#routes) +- **Styling**: CSS with design tokens + +## Architecture + +### Directory Structure +``` +admin-ui/src/ +├── main.tsx # Entry point +├── App.tsx # Root component with theme, shortcuts, toasts +├── state/ # Centralized state (signals) +│ ├── index.ts # Re-exports all state +│ ├── app.ts # Theme, panels, notifications +│ ├── project.ts # Project list, current project +│ └── team.ts # Teams, tools, configs +├── api/ +│ ├── client.ts # API client with all endpoints +│ └── types.ts # TypeScript interfaces +├── components/ +│ ├── base/ # Button, Card, Input, Badge, Spinner +│ ├── layout/ # Shell, Header, Sidebar, Panel, ChatSidebar +│ └── shared/ # CommandPalette, Toast +├── workdesks/ # Team-specific views +│ ├── UIWorkdesk.tsx # Figma extraction, code generation +│ ├── UXWorkdesk.tsx # Token list, Figma files +│ ├── QAWorkdesk.tsx # ESRE editor, console viewer +│ └── AdminWorkdesk.tsx # Settings, projects, audit log +├── hooks/ +│ └── useKeyboard.ts # Global keyboard shortcuts +└── styles/ + ├── tokens.css # Design tokens (light + dark) + └── base.css # Reset and base styles +``` + +## State Signals + +### App State (`state/app.ts`) +```typescript +theme: Signal<'light' | 'dark' | 'auto'> // Current theme +sidebarOpen: Signal // Sidebar visibility +chatSidebarOpen: Signal // AI chat visibility +commandPaletteOpen: Signal // Command palette visibility +activePanel: Signal // Bottom panel tab +notifications: Signal // Toast notifications + +// Actions +setTheme(theme) +toggleChatSidebar() +toggleCommandPalette() +addNotification(type, title, message) +dismissNotification(id) +``` + +### Project State (`state/project.ts`) +```typescript +projects: Signal // All projects +currentProject: Signal // Selected project + +// Actions +setCurrentProject(id) +refreshProjects() +``` + +### Team State (`state/team.ts`) +```typescript +activeTeam: Signal<'ui' | 'ux' | 'qa' | 'admin'> // Current team +teamConfig: Computed // Team's tools, panels, etc. + +// Actions +setActiveTeam(team) +``` + +## API Endpoints (`api/client.ts`) + +### Project Endpoints +```typescript +endpoints.projects.list(status?) // GET /api/projects +endpoints.projects.get(id) // GET /api/projects/:id +endpoints.projects.create(data) // POST /api/projects +endpoints.projects.update(id, data) // PUT /api/projects/:id +endpoints.projects.delete(id) // DELETE /api/projects/:id +endpoints.projects.config(id) // GET /api/projects/:id/config +endpoints.projects.components(id) // GET /api/projects/:id/components +endpoints.projects.figmaFiles(id) // GET /api/projects/:id/figma-files +endpoints.projects.addFigmaFile(id, data) // POST /api/projects/:id/figma-files +endpoints.projects.syncFigmaFile(id, fileId) // PUT /api/projects/:id/figma-files/:fileId/sync +``` + +### Figma Endpoints +```typescript +endpoints.figma.health() // GET /api/figma/health +endpoints.figma.extractVariables(fileKey, nodeId?) // POST /api/figma/extract-variables +endpoints.figma.extractComponents(fileKey, nodeId?) // POST /api/figma/extract-components +endpoints.figma.extractStyles(fileKey, nodeId?) // POST /api/figma/extract-styles +endpoints.figma.generateCode(componentDef, framework?) // POST /api/figma/generate-code +``` + +### Token Endpoints +```typescript +endpoints.tokens.drift(projectId) // GET /api/projects/:id/token-drift +endpoints.tokens.recordDrift(projectId, data) // POST /api/projects/:id/token-drift +``` + +### ESRE Endpoints (QA) +```typescript +endpoints.esre.list(projectId) // GET /api/projects/:id/esre +endpoints.esre.create(projectId, data) // POST /api/projects/:id/esre +endpoints.esre.update(projectId, esreId, data) // PUT /api/projects/:id/esre/:esreId +endpoints.esre.delete(projectId, esreId) // DELETE /api/projects/:id/esre/:esreId +``` + +### System Endpoints +```typescript +endpoints.system.health() // GET /api/health +endpoints.system.config() // GET /api/config +endpoints.system.updateConfig(data) // PUT /api/config +endpoints.system.figmaConfig() // GET /api/config/figma +endpoints.system.testFigma() // POST /api/config/figma/test +``` + +### Cache Endpoints +```typescript +endpoints.cache.clear() // POST /api/cache/clear +endpoints.cache.purge() // DELETE /api/cache +``` + +### Audit Endpoints +```typescript +endpoints.audit.list(params?) // GET /api/audit +endpoints.audit.stats() // GET /api/audit/stats +endpoints.audit.export(format) // GET /api/audit/export?format=json|csv +``` + +### Claude AI Endpoints +```typescript +endpoints.claude.chat(message, projectId?, context?) // POST /api/claude/chat +``` + +### MCP Endpoints +```typescript +endpoints.mcp.tools() // GET /api/mcp/tools +endpoints.mcp.execute(name, params) // POST /api/mcp/tools/:name/execute +endpoints.mcp.status() // GET /api/mcp/status +``` + +## Team Workdesks + +### UI Team (`UIWorkdesk.tsx`) +**Purpose**: Component library & Figma sync + +**Tools**: +- `dashboard` - Metrics: components count, token drift issues +- `figma-extraction` - Extract tokens from Figma using `endpoints.figma.extractVariables()` +- `figma-components` - Extract components using `endpoints.figma.extractComponents()` +- `code-generator` - Generate code using `endpoints.figma.generateCode()` +- `token-drift` - View drift using `endpoints.tokens.drift()` + +### UX Team (`UXWorkdesk.tsx`) +**Purpose**: Design consistency & token validation + +**Tools**: +- `dashboard` - Figma connection status, file sync status +- `token-list` - View tokens from Figma +- `figma-files` - Manage connected Figma files +- `figma-plugin` - Plugin installation info + +### QA Team (`QAWorkdesk.tsx`) +**Purpose**: Testing, validation & quality + +**Tools**: +- `dashboard` - Health score, ESRE definitions count +- `esre-editor` - Create/edit/delete ESRE definitions +- `console-viewer` - Browser console log capture +- `test-results` - View ESRE test results + +### Admin (`AdminWorkdesk.tsx`) +**Purpose**: System configuration & management + +**Tools**: +- `settings` - Server config, Figma token, Storybook URL +- `projects` - CRUD for projects +- `integrations` - External service connections +- `audit-log` - System activity with filtering/export +- `cache-management` - Clear/purge cache +- `health-monitor` - Service status with auto-refresh +- `export-import` - Project backup/restore + +## Features + +### Dark Mode +- Toggle via Header button or Command Palette +- Auto mode detects system preference +- Persisted to localStorage as `dss-theme` +- CSS variables in `[data-theme="dark"]` selector + +### Keyboard Shortcuts +``` +⌘K - Open command palette +⌘/ - Toggle AI chat +⌘1 - Switch to UI team +⌘2 - Switch to UX team +⌘3 - Switch to QA team +⌘4 - Switch to Admin +Escape - Close dialogs +``` + +### Command Palette +- Quick access to all teams, projects, and actions +- Fuzzy search through commands +- Keyboard navigation (↑↓ Enter Escape) + +### AI Chat Sidebar +- Context-aware (passes current project/team) +- Quick actions for common prompts +- Displays tool calls and results +- Basic markdown formatting + +### Toast Notifications +- Success, error, warning, info types +- Auto-dismiss for non-errors (5s) +- Manual dismiss available + +### Export/Import +- Export project data as JSON archive +- Import with preview +- Progress indicators + +## TypeScript Types (`api/types.ts`) + +### Core Types +```typescript +interface Project { + id: string; + name: string; + description?: string; + path: string; + status: 'active' | 'archived' | 'draft'; + components_count?: number; + tokens_count?: number; +} + +interface FigmaFile { + id: string; + name: string; + file_key: string; + status: 'pending' | 'synced' | 'error'; + last_synced?: string; +} + +interface ESREDefinition { + id: string; + name: string; + description?: string; + type: 'visual' | 'behavioral' | 'accessibility'; + selector: string; + expectations: Record; +} + +interface SystemHealth { + status: 'healthy' | 'degraded' | 'unhealthy'; + services: { storage: string; mcp: string; figma: string }; + timestamp: string; +} + +interface ChatResponse { + message?: { role: string; content: string }; + tool_calls?: ToolCall[]; + tool_results?: ToolResult[]; +} +``` + +## CSS Design Tokens + +### Colors (Light Mode) +```css +--color-primary: hsl(220, 14%, 10%) +--color-background: hsl(0, 0%, 100%) +--color-surface-0: hsl(0, 0%, 100%) +--color-surface-1: hsl(220, 14%, 98%) +--color-border: hsl(220, 9%, 89%) +--color-success: hsl(142, 76%, 36%) +--color-error: hsl(0, 84%, 60%) +--color-warning: hsl(38, 92%, 50%) +``` + +### Colors (Dark Mode) +```css +--color-primary: hsl(220, 14%, 90%) +--color-background: hsl(220, 14%, 10%) +--color-surface-0: hsl(220, 14%, 10%) +--color-surface-1: hsl(220, 14%, 12%) +--color-border: hsl(220, 9%, 20%) +``` + +### Spacing +```css +--spacing-1: 0.25rem (4px) +--spacing-2: 0.5rem (8px) +--spacing-3: 0.75rem (12px) +--spacing-4: 1rem (16px) +--spacing-6: 1.5rem (24px) +--spacing-8: 2rem (32px) +``` + +### Typography +```css +--font-size-xs: 0.75rem +--font-size-sm: 0.875rem +--font-size-base: 1rem +--font-size-lg: 1.125rem +--font-weight-normal: 400 +--font-weight-medium: 500 +--font-weight-semibold: 600 +--font-weight-bold: 700 +``` + +## Build Commands + +```bash +npm run dev # Start dev server (port 5173) +npm run build # TypeScript check + Vite build +npm run preview # Preview production build +``` + +## Bundle Size +- Framework (Preact + Signals): ~5KB +- Main bundle: ~66KB gzipped ~21KB +- Total CSS: ~28KB gzipped ~5KB +- Lazy-loaded workdesks: ~10-18KB each diff --git a/admin-ui/index.html b/admin-ui/index.html index e6a3ff6..5ebb338 100644 --- a/admin-ui/index.html +++ b/admin-ui/index.html @@ -3,17 +3,14 @@ - DSS Workdesk - - - - - - - + + + DSS Admin + + - - +
+ diff --git a/admin-ui/package.json b/admin-ui/package.json index 052d7bf..1044bcf 100644 --- a/admin-ui/package.json +++ b/admin-ui/package.json @@ -1,30 +1,30 @@ { "name": "dss-admin-ui", - "version": "1.0.0", - "description": "DSS Admin UI - Configuration and Component Management", + "version": "2.0.0", + "description": "DSS Admin UI - Design System Server Management", "private": true, "type": "module", "scripts": { "dev": "vite", + "build": "tsc && vite build", "preview": "vite preview", - "build": "vite build", "test": "vitest", "test:watch": "vitest --watch", "test:ui": "vitest --ui", - "test:coverage": "vitest --coverage", - "test:debug": "vitest --inspect-brk --inspect-port=9229 --no-coverage" + "test:coverage": "vitest --coverage" + }, + "dependencies": { + "preact": "^10.19.0", + "@preact/signals": "^1.2.0" }, "devDependencies": { - "@testing-library/dom": "^9.3.0", - "@testing-library/jest-dom": "^6.1.0", - "@vitejs/plugin-basic-ssl": "^1.0.0", - "@vitest/ui": "^1.0.0", - "jsdom": "^22.1.0", - "terser": "^5.44.1", + "@preact/preset-vite": "^2.8.0", + "@testing-library/preact": "^3.2.0", + "@types/node": "^20.10.0", + "typescript": "^5.3.0", "vite": "^5.0.0", - "vitest": "^1.0.0" - }, - "vitest": { - "globals": true + "vite-plugin-pwa": "^0.17.0", + "vitest": "^1.0.0", + "workbox-window": "^7.0.0" } } diff --git a/admin-ui/src/App.tsx b/admin-ui/src/App.tsx new file mode 100644 index 0000000..8e281c2 --- /dev/null +++ b/admin-ui/src/App.tsx @@ -0,0 +1,52 @@ +import { useEffect } from 'preact/hooks'; +import { effect } from '@preact/signals'; +import { theme, initializeApp } from './state'; +import { Shell } from './components/layout/Shell'; +import { CommandPalette } from './components/shared/CommandPalette'; +import { ToastContainer } from './components/shared/Toast'; +import { useKeyboardShortcuts } from './hooks/useKeyboard'; + +export function App() { + // Enable global keyboard shortcuts + useKeyboardShortcuts(); + useEffect(() => { + // Initialize app state + initializeApp(); + + // Apply theme reactively when signal changes + const dispose = effect(() => { + const root = document.documentElement; + const currentTheme = theme.value; + + if (currentTheme === 'auto') { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + root.dataset.theme = prefersDark ? 'dark' : 'light'; + } else { + root.dataset.theme = currentTheme; + } + }); + + // Listen for system theme changes (for auto mode) + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleSystemChange = () => { + if (theme.value === 'auto') { + const root = document.documentElement; + root.dataset.theme = mediaQuery.matches ? 'dark' : 'light'; + } + }; + mediaQuery.addEventListener('change', handleSystemChange); + + return () => { + dispose(); + mediaQuery.removeEventListener('change', handleSystemChange); + }; + }, []); + + return ( + <> + + + + + ); +} diff --git a/admin-ui/src/api/client.ts b/admin-ui/src/api/client.ts new file mode 100644 index 0000000..f7db451 --- /dev/null +++ b/admin-ui/src/api/client.ts @@ -0,0 +1,324 @@ +// DSS Admin UI - API Client + +export class ApiError extends Error { + constructor( + public status: number, + public data: unknown + ) { + super(`API Error: ${status}`); + this.name = 'ApiError'; + } +} + +class ApiClient { + private baseUrl = '/api'; + private pendingRequests = new Map>(); + + /** + * Make an API request with automatic error handling + */ + async request( + method: string, + path: string, + data?: unknown, + options?: RequestInit + ): Promise { + const url = `${this.baseUrl}${path}`; + + // Deduplicate GET requests + if (method === 'GET') { + const key = url; + const pending = this.pendingRequests.get(key); + if (pending) { + return pending as Promise; + } + } + + const requestPromise = this.executeRequest(method, url, data, options); + + // Store pending GET requests for deduplication + if (method === 'GET') { + this.pendingRequests.set(url, requestPromise); + requestPromise.finally(() => { + this.pendingRequests.delete(url); + }); + } + + return requestPromise; + } + + private async executeRequest( + method: string, + url: string, + data?: unknown, + options?: RequestInit + ): Promise { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...options?.headers + }; + + const config: RequestInit = { + method, + headers, + ...options + }; + + if (data && method !== 'GET') { + config.body = JSON.stringify(data); + } + + try { + const response = await fetch(url, config); + + if (!response.ok) { + let errorData: unknown; + try { + errorData = await response.json(); + } catch { + errorData = { message: response.statusText }; + } + throw new ApiError(response.status, errorData); + } + + // Handle empty responses + const text = await response.text(); + if (!text) { + return {} as T; + } + + return JSON.parse(text) as T; + } catch (error) { + if (error instanceof ApiError) { + throw error; + } + // Network or other errors + throw new ApiError(0, { message: 'Network error', originalError: error }); + } + } + + // Convenience methods + get(path: string, options?: RequestInit): Promise { + return this.request('GET', path, undefined, options); + } + + post(path: string, data?: unknown, options?: RequestInit): Promise { + return this.request('POST', path, data, options); + } + + put(path: string, data?: unknown, options?: RequestInit): Promise { + return this.request('PUT', path, data, options); + } + + patch(path: string, data?: unknown, options?: RequestInit): Promise { + return this.request('PATCH', path, data, options); + } + + delete(path: string, options?: RequestInit): Promise { + return this.request('DELETE', path, undefined, options); + } +} + +// Singleton instance +export const api = new ApiClient(); + +// Import types +import type { + Project, ProjectCreateData, ProjectConfig, + FigmaFile, FigmaExtractResult, FigmaHealthStatus, + TokenDrift, + Component, + ESREDefinition, ESRECreateData, + AuditEntry, AuditStats, + SystemHealth, RuntimeConfig, + DiscoveryResult, DiscoveryStats, + Team, TeamCreateData, + Integration, IntegrationCreateData, + ChatResponse, MCPTool, + Service +} from './types'; + +// Type-safe API endpoints - Mapped to actual DSS backend +export const endpoints = { + // Projects + projects: { + list: (status?: string) => + api.get(status ? `/projects?status=${status}` : '/projects'), + get: (id: string) => api.get(`/projects/${id}`), + create: (data: ProjectCreateData) => api.post('/projects', data), + update: (id: string, data: Partial) => api.put(`/projects/${id}`, data), + delete: (id: string) => api.delete(`/projects/${id}`), + config: (id: string) => api.get(`/projects/${id}/config`), + updateConfig: (id: string, data: ProjectConfig) => api.put(`/projects/${id}/config`, data), + context: (id: string) => api.get(`/projects/${id}/context`), + components: (id: string) => api.get(`/projects/${id}/components`), + dashboard: (id: string) => api.get(`/projects/${id}/dashboard/summary`), + // Files (sandboxed) + files: (id: string, path?: string) => + api.get(`/projects/${id}/files${path ? `?path=${encodeURIComponent(path)}` : ''}`), + fileTree: (id: string) => api.get(`/projects/${id}/files/tree`), + readFile: (id: string, path: string) => + api.get<{ content: string }>(`/projects/${id}/files/read?path=${encodeURIComponent(path)}`), + writeFile: (id: string, path: string, content: string) => + api.post(`/projects/${id}/files/write`, { path, content }), + // Figma files per project + figmaFiles: (id: string) => api.get(`/projects/${id}/figma-files`), + addFigmaFile: (id: string, data: { name: string; fileKey: string }) => + api.post(`/projects/${id}/figma-files`, data), + syncFigmaFile: (id: string, fileId: string) => + api.put(`/projects/${id}/figma-files/${fileId}/sync`, {}), + deleteFigmaFile: (id: string, fileId: string) => + api.delete(`/projects/${id}/figma-files/${fileId}`) + }, + + // Figma Integration + figma: { + health: () => api.get('/figma/health'), + extractVariables: (fileKey: string, nodeId?: string) => + api.post('/figma/extract-variables', { file_key: fileKey, node_id: nodeId }), + extractComponents: (fileKey: string, nodeId?: string) => + api.post('/figma/extract-components', { file_key: fileKey, node_id: nodeId }), + extractStyles: (fileKey: string, nodeId?: string) => + api.post('/figma/extract-styles', { file_key: fileKey, node_id: nodeId }), + syncTokens: (fileKey: string, targetPath: string, format?: string) => + api.post<{ synced: number }>('/figma/sync-tokens', { file_key: fileKey, target_path: targetPath, format }), + validate: (componentDef: unknown) => + api.post<{ valid: boolean; errors: string[] }>('/figma/validate', componentDef), + generateCode: (componentDef: unknown, framework?: string) => + api.post<{ code: string }>('/figma/generate-code', { component: componentDef, framework }) + }, + + // Token Drift + tokens: { + drift: (projectId: string) => api.get(`/projects/${projectId}/token-drift`), + recordDrift: (projectId: string, data: Partial) => + api.post(`/projects/${projectId}/token-drift`, data), + updateDriftStatus: (projectId: string, driftId: string, status: string) => + api.put(`/projects/${projectId}/token-drift/${driftId}/status`, { status }) + }, + + // ESRE Definitions (QA) + esre: { + list: (projectId: string) => api.get(`/projects/${projectId}/esre`), + create: (projectId: string, data: ESRECreateData) => + api.post(`/projects/${projectId}/esre`, data), + update: (projectId: string, esreId: string, data: Partial) => + api.put(`/projects/${projectId}/esre/${esreId}`, data), + delete: (projectId: string, esreId: string) => + api.delete(`/projects/${projectId}/esre/${esreId}`) + }, + + // Integrations per project + integrations: { + list: (projectId: string) => api.get(`/projects/${projectId}/integrations`), + create: (projectId: string, data: IntegrationCreateData) => + api.post(`/projects/${projectId}/integrations`, data), + update: (projectId: string, type: string, data: Partial) => + api.put(`/projects/${projectId}/integrations/${type}`, data), + delete: (projectId: string, type: string) => + api.delete(`/projects/${projectId}/integrations/${type}`) + }, + + // Discovery + discovery: { + run: () => api.get('/discovery'), + scan: () => api.post('/discovery/scan', {}), + stats: () => api.get('/discovery/stats'), + activity: () => api.get('/discovery/activity'), + ports: () => api.get('/discovery/ports'), + env: () => api.get('/discovery/env') + }, + + // Teams + teams: { + list: () => api.get('/teams'), + get: (id: string) => api.get(`/teams/${id}`), + create: (data: TeamCreateData) => api.post('/teams', data) + }, + + // System & Config + system: { + health: () => api.get('/health'), + stats: () => api.get('/stats'), + config: () => api.get('/config'), + updateConfig: (data: Partial) => api.put('/config', data), + figmaConfig: () => api.get<{ configured: boolean }>('/config/figma'), + testFigma: () => api.post<{ success: boolean; message: string }>('/config/figma/test', {}), + reset: () => api.post('/system/reset', {}), + mode: () => api.get<{ mode: string }>('/mode'), + setMode: (mode: string) => api.put<{ mode: string }>('/mode', { mode }) + }, + + // Cache Management + cache: { + clear: () => api.post<{ cleared: number }>('/cache/clear', {}), + purge: () => api.delete<{ purged: number }>('/cache') + }, + + // Services + services: { + list: () => api.get('/services'), + configure: (name: string, config: unknown) => api.put(`/services/${name}`, config), + storybook: () => api.get<{ running: boolean; url?: string }>('/services/storybook'), + initStorybook: () => api.post('/storybook/init', {}), + clearStories: () => api.delete('/storybook/stories') + }, + + // Audit Log + audit: { + list: (params?: Record) => { + const query = params ? '?' + new URLSearchParams(params).toString() : ''; + return api.get(`/audit${query}`); + }, + create: (entry: Partial) => api.post('/audit', entry), + stats: () => api.get('/audit/stats'), + categories: () => api.get('/audit/categories'), + actions: () => api.get('/audit/actions'), + export: (format: 'json' | 'csv') => api.get(`/audit/export?format=${format}`) + }, + + // Activity + activity: { + recent: () => api.get('/activity'), + syncHistory: () => api.get('/sync-history') + }, + + // Claude AI Chat + claude: { + chat: (message: string, projectId?: string, context?: unknown) => + api.post('/claude/chat', { message, project_id: projectId, context }) + }, + + // MCP Tools + mcp: { + integrations: () => api.get('/mcp/integrations'), + tools: () => api.get('/mcp/tools'), + tool: (name: string) => api.get(`/mcp/tools/${name}`), + execute: (name: string, params: unknown) => api.post(`/mcp/tools/${name}/execute`, params), + status: () => api.get<{ connected: boolean; tools: number }>('/mcp/status') + }, + + // Browser Logs + browserLogs: { + send: (logs: unknown[]) => api.post('/browser-logs', { logs }), + get: (sessionId: string) => api.get(`/browser-logs/${sessionId}`) + }, + + // Debug + debug: { + diagnostic: () => api.get('/debug/diagnostic'), + workflows: () => api.get('/debug/workflows') + }, + + // Design System Ingestion + ingest: { + parse: (prompt: string) => api.post('/ingest/parse', { prompt }), + systems: (filter?: string) => api.get(`/ingest/systems${filter ? `?filter=${filter}` : ''}`), + system: (id: string) => api.get(`/ingest/systems/${id}`), + npmSearch: (query: string) => api.get(`/ingest/npm/search?q=${encodeURIComponent(query)}`), + npmPackage: (name: string) => api.get(`/ingest/npm/package/${encodeURIComponent(name)}`), + confirm: (data: unknown) => api.post('/ingest/confirm', data), + execute: (data: unknown) => api.post('/ingest/execute', data), + alternatives: () => api.get('/ingest/alternatives') + } +} as const; diff --git a/admin-ui/src/api/types.ts b/admin-ui/src/api/types.ts new file mode 100644 index 0000000..abd2179 --- /dev/null +++ b/admin-ui/src/api/types.ts @@ -0,0 +1,345 @@ +// DSS Admin UI - API Types + +// ============ Projects ============ + +export interface Project { + id: string; + name: string; + description?: string; + path: string; + status: 'active' | 'archived' | 'draft'; + created_at: string; + updated_at: string; + components_count?: number; + tokens_count?: number; +} + +export interface ProjectCreateData { + name: string; + description?: string; + path: string; +} + +export interface ProjectConfig { + figma?: { + file_key?: string; + access_token?: string; + }; + storybook?: { + url?: string; + config_path?: string; + }; + tokens?: { + source?: string; + output?: string; + format?: 'css' | 'scss' | 'json' | 'js'; + }; + components?: { + source_dir?: string; + patterns?: string[]; + }; +} + +// ============ Figma ============ + +export interface FigmaFile { + id: string; + project_id: string; + name: string; + file_key: string; + status: 'synced' | 'pending' | 'error'; + last_synced?: string; + error_message?: string; + created_at: string; +} + +export interface FigmaHealthStatus { + connected: boolean; + token_configured: boolean; + last_request?: string; + rate_limit?: { + remaining: number; + reset_at: string; + }; +} + +export interface FigmaExtractResult { + success: boolean; + count: number; + items: FigmaExtractedItem[]; + warnings?: string[]; + errors?: string[]; +} + +export interface FigmaExtractedItem { + name: string; + type: string; + value: unknown; + description?: string; + metadata?: Record; +} + +// ============ Tokens ============ + +export interface Token { + name: string; + value: string; + type: 'color' | 'spacing' | 'typography' | 'radius' | 'shadow' | 'other'; + category?: string; + description?: string; + source?: 'figma' | 'code' | 'manual'; +} + +export interface TokenDrift { + id: string; + project_id: string; + token_name: string; + expected_value: string; + actual_value: string; + file_path: string; + line_number?: number; + severity: 'low' | 'medium' | 'high' | 'critical'; + status: 'open' | 'resolved' | 'ignored'; + created_at: string; + resolved_at?: string; +} + +// ============ Components ============ + +export interface Component { + id: string; + name: string; + display_name?: string; + description?: string; + file_path: string; + type: 'react' | 'vue' | 'web-component' | 'other'; + props?: ComponentProp[]; + variants?: string[]; + has_story?: boolean; + figma_node_id?: string; + created_at: string; + updated_at: string; +} + +export interface ComponentProp { + name: string; + type: string; + required: boolean; + default_value?: unknown; + description?: string; +} + +// ============ ESRE (QA) ============ + +export interface ESREDefinition { + id: string; + project_id: string; + name: string; + component_name: string; + description?: string; + expected_value: string; + selector?: string; + css_property?: string; + status: 'active' | 'inactive'; + last_result?: 'pass' | 'fail' | 'skip'; + last_tested?: string; + created_at: string; + updated_at: string; +} + +export interface ESRECreateData { + name: string; + component_name: string; + description?: string; + expected_value: string; + selector?: string; + css_property?: string; +} + +// ============ Audit ============ + +export interface AuditEntry { + id: string; + timestamp: string; + category: string; + action: string; + user?: string; + project_id?: string; + details?: Record; + metadata?: Record; +} + +export interface AuditStats { + total_entries: number; + by_category: Record; + by_action: Record; + recent_activity: number; +} + +// ============ System ============ + +export interface SystemHealth { + status: 'healthy' | 'degraded' | 'unhealthy'; + version?: string; + uptime?: number; + services: { + storage: 'up' | 'down'; + mcp: 'up' | 'down'; + figma: 'up' | 'down' | 'not_configured'; + }; + timestamp: string; +} + +export interface RuntimeConfig { + server_host: string; + server_port: number; + figma_token?: string; + storybook_url?: string; + data_dir?: string; + log_level?: 'debug' | 'info' | 'warning' | 'error'; +} + +export interface CacheStatus { + size: number; + entries: number; + hit_rate: number; + last_cleared?: string; +} + +// ============ Discovery ============ + +export interface DiscoveryResult { + projects: DiscoveredProject[]; + services: DiscoveredService[]; + timestamp: string; +} + +export interface DiscoveredProject { + path: string; + name: string; + type: string; + has_dss_config: boolean; + framework?: string; +} + +export interface DiscoveredService { + name: string; + port: number; + type: string; + url?: string; + status: 'running' | 'stopped'; +} + +export interface DiscoveryStats { + projects_discovered: number; + services_running: number; + last_scan: string; +} + +// ============ Teams ============ + +export interface Team { + id: string; + name: string; + description?: string; + members?: TeamMember[]; + created_at: string; +} + +export interface TeamMember { + id: string; + name: string; + email: string; + role: 'admin' | 'member' | 'viewer'; +} + +export interface TeamCreateData { + name: string; + description?: string; +} + +// ============ Integrations ============ + +export interface Integration { + type: 'figma' | 'jira' | 'confluence' | 'github' | 'gitlab' | 'slack'; + project_id: string; + status: 'connected' | 'disconnected' | 'error'; + config: Record; + last_sync?: string; + error_message?: string; +} + +export interface IntegrationCreateData { + type: Integration['type']; + config: Record; +} + +// ============ Services ============ + +export interface Service { + name: string; + type: string; + url?: string; + port?: number; + status: 'running' | 'stopped' | 'unknown'; + config?: Record; +} + +// ============ Chat / Claude ============ + +export interface ChatMessage { + id: string; + role: 'user' | 'assistant' | 'system'; + content: string; + timestamp: string; + tool_calls?: ToolCall[]; + tool_results?: ToolResult[]; +} + +export interface ChatResponse { + message: ChatMessage; + tool_calls?: ToolCall[]; + tool_results?: ToolResult[]; +} + +export interface ToolCall { + id: string; + name: string; + arguments: Record; +} + +export interface ToolResult { + tool_call_id: string; + output: unknown; + error?: string; +} + +// ============ MCP ============ + +export interface MCPTool { + name: string; + description: string; + input_schema: { + type: 'object'; + properties: Record; + required?: string[]; + }; + category?: string; +} + +// ============ Browser Logs ============ + +export interface BrowserLog { + level: 'log' | 'info' | 'warn' | 'error' | 'debug'; + message: string; + timestamp: string; + source?: string; + line?: number; + column?: number; + stack?: string; +} diff --git a/admin-ui/src/components/base/Badge.css b/admin-ui/src/components/base/Badge.css new file mode 100644 index 0000000..1803a66 --- /dev/null +++ b/admin-ui/src/components/base/Badge.css @@ -0,0 +1,49 @@ +/* Badge Component Styles */ + +.badge { + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: var(--font-weight-medium); + border-radius: var(--radius-full); + white-space: nowrap; +} + +/* Sizes */ +.badge-sm { + height: 20px; + padding: 0 var(--spacing-2); + font-size: var(--font-size-xs); +} + +.badge-md { + height: 24px; + padding: 0 var(--spacing-2-5); + font-size: var(--font-size-sm); +} + +/* Variants */ +.badge-default { + background-color: var(--color-muted); + color: var(--color-muted-foreground); +} + +.badge-success { + background-color: var(--color-success); + color: var(--color-success-foreground); +} + +.badge-warning { + background-color: var(--color-warning); + color: var(--color-warning-foreground); +} + +.badge-error { + background-color: var(--color-error); + color: var(--color-error-foreground); +} + +.badge-info { + background-color: var(--color-info); + color: var(--color-info-foreground); +} diff --git a/admin-ui/src/components/base/Badge.tsx b/admin-ui/src/components/base/Badge.tsx new file mode 100644 index 0000000..21fdb72 --- /dev/null +++ b/admin-ui/src/components/base/Badge.tsx @@ -0,0 +1,30 @@ +import { JSX } from 'preact'; +import './Badge.css'; + +export type BadgeVariant = 'default' | 'success' | 'warning' | 'error' | 'info'; + +export interface BadgeProps extends JSX.HTMLAttributes { + variant?: BadgeVariant; + size?: 'sm' | 'md'; +} + +export function Badge({ + variant = 'default', + size = 'md', + className = '', + children, + ...props +}: BadgeProps) { + const classes = [ + 'badge', + `badge-${variant}`, + `badge-${size}`, + className + ].filter(Boolean).join(' '); + + return ( + + {children} + + ); +} diff --git a/admin-ui/src/components/base/Button.css b/admin-ui/src/components/base/Button.css new file mode 100644 index 0000000..72c5769 --- /dev/null +++ b/admin-ui/src/components/base/Button.css @@ -0,0 +1,146 @@ +/* Button Component Styles */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-2); + font-family: inherit; + font-weight: var(--font-weight-medium); + border-radius: var(--radius-md); + border: 1px solid transparent; + cursor: pointer; + transition: all var(--duration-fast) var(--timing-out); + white-space: nowrap; + user-select: none; +} + +.btn:focus-visible { + outline: 2px solid var(--color-ring); + outline-offset: 2px; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Sizes */ +.btn-sm { + height: 32px; + padding: 0 var(--spacing-3); + font-size: var(--font-size-sm); +} + +.btn-md { + height: 40px; + padding: 0 var(--spacing-4); + font-size: var(--font-size-sm); +} + +.btn-lg { + height: 48px; + padding: 0 var(--spacing-6); + font-size: var(--font-size-base); +} + +/* Variants */ +.btn-primary { + background-color: var(--color-primary); + color: var(--color-primary-foreground); +} + +.btn-primary:hover:not(:disabled) { + opacity: 0.9; +} + +.btn-secondary { + background-color: var(--color-secondary); + color: var(--color-secondary-foreground); +} + +.btn-secondary:hover:not(:disabled) { + opacity: 0.9; +} + +.btn-ghost { + background-color: transparent; + color: var(--color-foreground); +} + +.btn-ghost:hover:not(:disabled) { + background-color: var(--color-accent); +} + +.btn-outline { + background-color: transparent; + border-color: var(--color-border); + color: var(--color-foreground); +} + +.btn-outline:hover:not(:disabled) { + background-color: var(--color-accent); + border-color: var(--color-border-strong); +} + +.btn-danger { + background-color: var(--color-error); + color: var(--color-error-foreground); +} + +.btn-danger:hover:not(:disabled) { + opacity: 0.9; +} + +/* Full Width */ +.btn-full-width { + width: 100%; +} + +/* Loading State */ +.btn-loading { + position: relative; + color: transparent; +} + +.btn-spinner { + position: absolute; + width: 16px; + height: 16px; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +.btn-loading .btn-spinner { + color: var(--color-primary-foreground); +} + +/* Icons */ +.btn-icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.btn-icon svg { + width: 16px; + height: 16px; +} + +.btn-sm .btn-icon svg { + width: 14px; + height: 14px; +} + +.btn-lg .btn-icon svg { + width: 18px; + height: 18px; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} diff --git a/admin-ui/src/components/base/Button.tsx b/admin-ui/src/components/base/Button.tsx new file mode 100644 index 0000000..44211c7 --- /dev/null +++ b/admin-ui/src/components/base/Button.tsx @@ -0,0 +1,63 @@ +import { JSX } from 'preact'; +import './Button.css'; + +export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'outline'; +export type ButtonSize = 'sm' | 'md' | 'lg'; + +export interface ButtonProps { + variant?: ButtonVariant; + size?: ButtonSize; + loading?: boolean; + icon?: JSX.Element; + iconPosition?: 'left' | 'right'; + fullWidth?: boolean; + disabled?: boolean; + type?: 'button' | 'submit' | 'reset'; + onClick?: (e: MouseEvent) => void; + className?: string; + children?: JSX.Element | string | null; + 'aria-label'?: string; +} + +export function Button({ + variant = 'primary', + size = 'md', + loading = false, + icon, + iconPosition = 'left', + fullWidth = false, + disabled, + type = 'button', + onClick, + className = '', + children, + ...props +}: ButtonProps) { + const classes = [ + 'btn', + `btn-${variant}`, + `btn-${size}`, + loading && 'btn-loading', + fullWidth && 'btn-full-width', + className + ].filter(Boolean).join(' '); + + return ( + + ); +} diff --git a/admin-ui/src/components/base/Card.css b/admin-ui/src/components/base/Card.css new file mode 100644 index 0000000..cb6e8fa --- /dev/null +++ b/admin-ui/src/components/base/Card.css @@ -0,0 +1,86 @@ +/* Card Component Styles */ + +.card { + background-color: var(--color-surface-0); + border-radius: var(--radius-lg); + overflow: hidden; +} + +/* Variants */ +.card-default { + background-color: var(--color-surface-1); +} + +.card-bordered { + border: 1px solid var(--color-border); +} + +.card-elevated { + box-shadow: var(--shadow-md); +} + +/* Padding */ +.card-padding-none { + padding: 0; +} + +.card-padding-sm { + padding: var(--spacing-3); +} + +.card-padding-md { + padding: var(--spacing-4); +} + +.card-padding-lg { + padding: var(--spacing-6); +} + +/* Header */ +.card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--spacing-4); + padding-bottom: var(--spacing-4); + border-bottom: 1px solid var(--color-border); + margin-bottom: var(--spacing-4); +} + +.card-header-content { + flex: 1; + min-width: 0; +} + +.card-title { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + color: var(--color-foreground); + margin: 0; +} + +.card-subtitle { + font-size: var(--font-size-sm); + color: var(--color-muted-foreground); + margin: var(--spacing-1) 0 0 0; +} + +.card-header-action { + flex-shrink: 0; +} + +/* Content */ +.card-content { + /* Default content styles */ +} + +/* Footer */ +.card-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--spacing-3); + padding-top: var(--spacing-4); + border-top: 1px solid var(--color-border); + margin-top: var(--spacing-4); +} diff --git a/admin-ui/src/components/base/Card.tsx b/admin-ui/src/components/base/Card.tsx new file mode 100644 index 0000000..7ff34ba --- /dev/null +++ b/admin-ui/src/components/base/Card.tsx @@ -0,0 +1,67 @@ +import { JSX, ComponentChildren } from 'preact'; +import './Card.css'; + +export interface CardProps extends JSX.HTMLAttributes { + variant?: 'default' | 'bordered' | 'elevated'; + padding?: 'none' | 'sm' | 'md' | 'lg'; +} + +export function Card({ + variant = 'default', + padding = 'md', + className = '', + children, + ...props +}: CardProps) { + const classes = [ + 'card', + `card-${variant}`, + `card-padding-${padding}`, + className + ].filter(Boolean).join(' '); + + return ( +
+ {children} +
+ ); +} + +export interface CardHeaderProps { + title: string; + subtitle?: string; + action?: ComponentChildren; + className?: string; +} + +export function CardHeader({ title, subtitle, action, className = '' }: CardHeaderProps) { + return ( +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+ {action &&
{action}
} +
+ ); +} + +export interface CardContentProps extends JSX.HTMLAttributes {} + +export function CardContent({ className = '', children, ...props }: CardContentProps) { + return ( +
+ {children} +
+ ); +} + +export interface CardFooterProps extends JSX.HTMLAttributes {} + +export function CardFooter({ className = '', children, ...props }: CardFooterProps) { + return ( +
+ {children} +
+ ); +} diff --git a/admin-ui/src/components/base/Input.css b/admin-ui/src/components/base/Input.css new file mode 100644 index 0000000..eb95930 --- /dev/null +++ b/admin-ui/src/components/base/Input.css @@ -0,0 +1,171 @@ +/* Input Component Styles */ + +.input-wrapper { + display: flex; + flex-direction: column; + gap: var(--spacing-1-5); +} + +.input-full-width { + width: 100%; +} + +.input-label { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-foreground); +} + +.input-container { + position: relative; + display: flex; + align-items: center; +} + +/* Input Field */ +.input, +.textarea, +.select { + width: 100%; + font-family: inherit; + font-size: var(--font-size-sm); + color: var(--color-foreground); + background-color: var(--color-surface-0); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + transition: border-color var(--duration-fast) var(--timing-out), + box-shadow var(--duration-fast) var(--timing-out); +} + +.input:hover, +.textarea:hover, +.select:hover { + border-color: var(--color-border-strong); +} + +.input:focus, +.textarea:focus, +.select:focus { + outline: none; + border-color: var(--color-ring); + box-shadow: 0 0 0 3px var(--color-ring) / 0.1; +} + +.input::placeholder, +.textarea::placeholder { + color: var(--color-muted-foreground); +} + +/* Input Sizes */ +.input-sm { + height: 32px; + padding: 0 var(--spacing-3); +} + +.input-md { + height: 40px; + padding: 0 var(--spacing-3); +} + +.input-lg { + height: 48px; + padding: 0 var(--spacing-4); + font-size: var(--font-size-base); +} + +/* Input with Icons */ +.input-with-left-icon { + padding-left: var(--spacing-10); +} + +.input-with-right-icon { + padding-right: var(--spacing-10); +} + +.input-icon { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + color: var(--color-muted-foreground); + pointer-events: none; +} + +.input-icon svg { + width: 16px; + height: 16px; +} + +.input-icon-left { + left: var(--spacing-3); +} + +.input-icon-right { + right: var(--spacing-3); +} + +/* Textarea */ +.textarea { + min-height: 80px; + padding: var(--spacing-3); + resize: vertical; + line-height: var(--line-height-normal); +} + +/* Select */ +.select { + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right var(--spacing-3) center; + padding-right: var(--spacing-10); + cursor: pointer; +} + +.select-sm { + height: 32px; + padding-left: var(--spacing-3); +} + +.select-md { + height: 40px; + padding-left: var(--spacing-3); +} + +.select-lg { + height: 48px; + padding-left: var(--spacing-4); + font-size: var(--font-size-base); +} + +/* Error State */ +.input-error .input, +.input-error .textarea, +.input-error .select { + border-color: var(--color-error); +} + +.input-error .input:focus, +.input-error .textarea:focus, +.input-error .select:focus { + box-shadow: 0 0 0 3px var(--color-error) / 0.1; +} + +.input-error-message { + font-size: var(--font-size-sm); + color: var(--color-error); +} + +.input-hint { + font-size: var(--font-size-sm); + color: var(--color-muted-foreground); +} + +/* Disabled State */ +.input:disabled, +.textarea:disabled, +.select:disabled { + opacity: 0.5; + cursor: not-allowed; + background-color: var(--color-muted); +} diff --git a/admin-ui/src/components/base/Input.tsx b/admin-ui/src/components/base/Input.tsx new file mode 100644 index 0000000..f26b62e --- /dev/null +++ b/admin-ui/src/components/base/Input.tsx @@ -0,0 +1,237 @@ +import { JSX, Ref } from 'preact'; +import './Input.css'; + +export interface InputProps { + label?: string; + error?: string; + hint?: string; + size?: 'sm' | 'md' | 'lg'; + fullWidth?: boolean; + leftIcon?: JSX.Element; + rightIcon?: JSX.Element; + // Common input props + id?: string; + name?: string; + type?: string; + value?: string | number; + defaultValue?: string | number; + placeholder?: string; + disabled?: boolean; + readOnly?: boolean; + required?: boolean; + autoFocus?: boolean; + autoComplete?: string; + className?: string; + onChange?: (e: Event) => void; + onInput?: (e: Event) => void; + onFocus?: (e: FocusEvent) => void; + onBlur?: (e: FocusEvent) => void; + ref?: Ref; +} + +export function Input({ + label, + error, + hint, + size = 'md', + fullWidth = false, + leftIcon, + rightIcon, + className = '', + id, + ...props +}: InputProps) { + const inputId = id || `input-${Math.random().toString(36).substr(2, 9)}`; + + const wrapperClasses = [ + 'input-wrapper', + fullWidth && 'input-full-width', + error && 'input-error', + className + ].filter(Boolean).join(' '); + + const inputClasses = [ + 'input', + `input-${size}`, + leftIcon && 'input-with-left-icon', + rightIcon && 'input-with-right-icon' + ].filter(Boolean).join(' '); + + return ( +
+ {label && ( + + )} +
+ {leftIcon && {leftIcon}} + + {rightIcon && {rightIcon}} +
+ {error && ( + + {error} + + )} + {hint && !error && ( + + {hint} + + )} +
+ ); +} + +export interface TextareaProps { + label?: string; + error?: string; + hint?: string; + fullWidth?: boolean; + // Common textarea props + id?: string; + name?: string; + value?: string; + defaultValue?: string; + placeholder?: string; + disabled?: boolean; + readOnly?: boolean; + required?: boolean; + rows?: number; + cols?: number; + className?: string; + onChange?: (e: Event) => void; + onInput?: (e: Event) => void; + onFocus?: (e: FocusEvent) => void; + onBlur?: (e: FocusEvent) => void; +} + +export function Textarea({ + label, + error, + hint, + fullWidth = false, + className = '', + id, + ...props +}: TextareaProps) { + const inputId = id || `textarea-${Math.random().toString(36).substr(2, 9)}`; + + const wrapperClasses = [ + 'input-wrapper', + fullWidth && 'input-full-width', + error && 'input-error', + className + ].filter(Boolean).join(' '); + + return ( +
+ {label && ( + + )} +