Reference implementation for the Phoenix Architecture. Work in progress. aicoding.leaflet.pub/
ai coding crazy
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

TaskFlow web dashboard — spec-driven UI generation

Added spec/web-dashboard.md describing a task management dashboard:
- Dashboard page with header, create form, task list
- Styled task cards with priority/status badges
- Analytics stats panel
- Responsive CSS with custom properties

Phoenix bootstrap: 3 spec files → 48 canonical nodes → 11 IUs → 3 services:
- Analytics API (:3000)
- Tasks API (:3001)
- Web Dashboard (:3002/4000) — serves complete HTML with inline CSS+JS

Run: cd examples/taskflow && PORT=4000 node dist/generated/web-dashboard/server.js
Open: http://localhost:4000

+1963 -352
+2 -1
examples/taskflow/package.json
··· 1 1 { 2 2 "name": "taskflow", 3 3 "version": "0.1.0", 4 - "description": "Generated by Phoenix VCS — 2 services", 4 + "description": "Generated by Phoenix VCS — 3 services", 5 5 "type": "module", 6 6 "scripts": { 7 7 "build": "tsc", ··· 10 10 "test:watch": "vitest", 11 11 "start:analytics": "tsc && node dist/generated/analytics/server.js", 12 12 "start:tasks": "tsc && node dist/generated/tasks/server.js", 13 + "start:web-dashboard": "tsc && node dist/generated/web-dashboard/server.js", 13 14 "start": "tsc && node dist/generated/analytics/server.js" 14 15 }, 15 16 "devDependencies": {
+34
examples/taskflow/spec/web-dashboard.md
··· 1 + # Task Dashboard Web Client 2 + 3 + A single-page web dashboard for managing tasks. Served as HTML from the server. 4 + 5 + ## Dashboard Page 6 + 7 + - The dashboard must render a complete HTML page with inline CSS and JavaScript 8 + - The page must display a header with the title "TaskFlow" and a task count summary 9 + - The page must include a form to create new tasks with fields: title, description, priority dropdown, and optional deadline date 10 + - The create form must validate that title is non-empty before submission 11 + - The page must use a clean, modern design with a blue (#2563eb) primary color 12 + 13 + ## Task List Display 14 + 15 + - The dashboard must render all tasks as styled cards in a responsive grid layout 16 + - Each task card must show: title, description, priority badge, status badge, assignee, and deadline 17 + - Priority badges must be color-coded: critical=red, high=orange, medium=yellow, low=green 18 + - Status badges must be color-coded: open=gray, in_progress=blue, review=purple, done=green 19 + - Overdue tasks must have a red border and an "OVERDUE" indicator 20 + - Each card must have buttons for status transitions (based on current status) 21 + 22 + ## Analytics Panel 23 + 24 + - The dashboard must include a stats panel showing: total tasks, completed count, overdue count, and completion rate percentage 25 + - The stats panel must render as a row of metric cards at the top of the page 26 + - Each metric card must show the metric name, value, and an appropriate emoji icon 27 + 28 + ## Styles 29 + 30 + - The dashboard must use CSS custom properties for theming (--primary, --danger, --success, --warning colors) 31 + - The layout must be responsive: single column on mobile, multi-column grid on desktop 32 + - Cards must have subtle shadows, rounded corners (8px), and hover effects 33 + - The font must be system-ui with appropriate size hierarchy (h1: 1.5rem, body: 0.95rem) 34 + - Buttons must have rounded corners, appropriate padding, and cursor pointer
+55 -77
examples/taskflow/src/generated/analytics/metrics.ts
··· 18 18 export class Metrics { 19 19 private tasks: TaskRecord[] = []; 20 20 21 - constructor(tasks: TaskRecord[] = []) { 22 - this.tasks = [...tasks]; 21 + constructor(initialTasks: TaskRecord[] = []) { 22 + this.tasks = [...initialTasks]; 23 23 } 24 24 25 25 updateTasks(tasks: TaskRecord[]): void { ··· 34 34 this.tasks = this.tasks.filter(task => task.id !== taskId); 35 35 } 36 36 37 - calculateMetrics(): MetricsSnapshot { 37 + getSnapshot(): MetricsSnapshot { 38 38 const now = new Date(); 39 - const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); 40 - 41 - const totalTasksCreated = this.tasks.length; 42 39 43 - const completedTasks = this.tasks.filter(task => task.status === 'completed' && task.completedAt); 44 - const totalTasksCompleted = completedTasks.length; 45 - 46 - const overdueTasks = this.tasks.filter(task => { 47 - if (!task.dueDate || task.status === 'completed' || task.status === 'cancelled') { 48 - return false; 49 - } 50 - return now > task.dueDate; 51 - }); 52 - const totalTasksOverdue = overdueTasks.length; 53 - 54 - const averageCompletionTimeHours = this.calculateAverageCompletionTime(completedTasks); 55 - 56 - const throughputTasksPerDay = this.calculateThroughput(completedTasks, sevenDaysAgo, now); 57 - 58 40 return { 59 - totalTasksCreated, 60 - totalTasksCompleted, 61 - totalTasksOverdue, 62 - averageCompletionTimeHours, 63 - throughputTasksPerDay, 41 + totalTasksCreated: this.calculateTotalTasksCreated(), 42 + totalTasksCompleted: this.calculateTotalTasksCompleted(), 43 + totalTasksOverdue: this.calculateTotalTasksOverdue(now), 44 + averageCompletionTimeHours: this.calculateAverageCompletionTime(), 45 + throughputTasksPerDay: this.calculateThroughput(now), 64 46 calculatedAt: now 65 47 }; 66 48 } 67 49 68 - private calculateAverageCompletionTime(completedTasks: TaskRecord[]): number { 69 - if (completedTasks.length === 0) { 70 - return 0; 71 - } 50 + private calculateTotalTasksCreated(): number { 51 + return this.tasks.length; 52 + } 72 53 73 - const totalCompletionTimeMs = completedTasks.reduce((sum, task) => { 74 - if (!task.completedAt) { 75 - return sum; 76 - } 77 - const completionTimeMs = task.completedAt.getTime() - task.createdAt.getTime(); 78 - return sum + completionTimeMs; 79 - }, 0); 80 - 81 - const averageCompletionTimeMs = totalCompletionTimeMs / completedTasks.length; 82 - return averageCompletionTimeMs / (1000 * 60 * 60); // Convert to hours 54 + private calculateTotalTasksCompleted(): number { 55 + return this.tasks.filter(task => task.status === 'completed').length; 83 56 } 84 57 85 - private calculateThroughput(completedTasks: TaskRecord[], startDate: Date, endDate: Date): number { 86 - const tasksCompletedInWindow = completedTasks.filter(task => { 87 - if (!task.completedAt) { 58 + private calculateTotalTasksOverdue(now: Date): number { 59 + return this.tasks.filter(task => { 60 + if (task.status === 'completed' || task.status === 'cancelled') { 88 61 return false; 89 62 } 90 - return task.completedAt >= startDate && task.completedAt <= endDate; 91 - }); 63 + return task.dueDate && task.dueDate < now; 64 + }).length; 65 + } 92 66 93 - const windowDays = (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24); 94 - 95 - if (windowDays === 0) { 67 + private calculateAverageCompletionTime(): number { 68 + const completedTasks = this.tasks.filter(task => 69 + task.status === 'completed' && task.completedAt 70 + ); 71 + 72 + if (completedTasks.length === 0) { 96 73 return 0; 97 74 } 98 75 99 - return tasksCompletedInWindow.length / windowDays; 100 - } 76 + const totalCompletionTimeMs = completedTasks.reduce((sum, task) => { 77 + const completionTime = task.completedAt!.getTime() - task.createdAt.getTime(); 78 + return sum + completionTime; 79 + }, 0); 101 80 102 - getTotalTasksCreated(): number { 103 - return this.tasks.length; 81 + const averageCompletionTimeMs = totalCompletionTimeMs / completedTasks.length; 82 + return averageCompletionTimeMs / (1000 * 60 * 60); // Convert to hours 104 83 } 105 84 106 - getTotalTasksCompleted(): number { 107 - return this.tasks.filter(task => task.status === 'completed').length; 108 - } 109 - 110 - getTotalTasksOverdue(): number { 111 - const now = new Date(); 112 - return this.tasks.filter(task => { 113 - if (!task.dueDate || task.status === 'completed' || task.status === 'cancelled') { 85 + private calculateThroughput(now: Date): number { 86 + const sevenDaysAgo = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000)); 87 + 88 + const completedInWindow = this.tasks.filter(task => { 89 + if (task.status !== 'completed' || !task.completedAt) { 114 90 return false; 115 91 } 116 - return now > task.dueDate; 117 - }).length; 118 - } 92 + return task.completedAt >= sevenDaysAgo && task.completedAt <= now; 93 + }); 119 94 120 - getAverageCompletionTimeHours(): number { 121 - const completedTasks = this.tasks.filter(task => task.status === 'completed' && task.completedAt); 122 - return this.calculateAverageCompletionTime(completedTasks); 95 + return completedInWindow.length / 7; 123 96 } 97 + } 124 98 125 - getThroughputTasksPerDay(): number { 126 - const now = new Date(); 127 - const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); 128 - const completedTasks = this.tasks.filter(task => task.status === 'completed' && task.completedAt); 129 - return this.calculateThroughput(completedTasks, sevenDaysAgo, now); 130 - } 99 + export function calculateMetrics(tasks: TaskRecord[]): MetricsSnapshot { 100 + const metrics = new Metrics(tasks); 101 + return metrics.getSnapshot(); 131 102 } 132 103 133 - export function createMetrics(tasks: TaskRecord[] = []): Metrics { 134 - return new Metrics(tasks); 104 + export function isTaskOverdue(task: TaskRecord, referenceDate: Date = new Date()): boolean { 105 + if (task.status === 'completed' || task.status === 'cancelled') { 106 + return false; 107 + } 108 + return task.dueDate ? task.dueDate < referenceDate : false; 135 109 } 136 110 137 - export function calculateMetricsFromTasks(tasks: TaskRecord[]): MetricsSnapshot { 138 - const metrics = new Metrics(tasks); 139 - return metrics.calculateMetrics(); 111 + export function getCompletionTimeHours(task: TaskRecord): number | null { 112 + if (task.status !== 'completed' || !task.completedAt) { 113 + return null; 114 + } 115 + 116 + const completionTimeMs = task.completedAt.getTime() - task.createdAt.getTime(); 117 + return completionTimeMs / (1000 * 60 * 60); 140 118 } 141 119 142 120 /** @internal Phoenix VCS traceability — do not remove. */
+18 -23
examples/taskflow/src/generated/analytics/priority-breakdown.ts
··· 34 34 } 35 35 36 36 removeTask(taskId: string): boolean { 37 - const initialLength = this.tasks.length; 38 - this.tasks = this.tasks.filter(task => task.id !== taskId); 39 - return this.tasks.length < initialLength; 37 + const index = this.tasks.findIndex(task => task.id === taskId); 38 + if (index !== -1) { 39 + this.tasks.splice(index, 1); 40 + return true; 41 + } 42 + return false; 40 43 } 41 44 42 45 updateTask(taskId: string, updates: Partial<Pick<Task, 'priority' | 'status'>>): boolean { 43 46 const task = this.tasks.find(t => t.id === taskId); 44 - if (!task) return false; 45 - 46 - if (updates.priority) task.priority = updates.priority; 47 - if (updates.status) task.status = updates.status; 48 - return true; 47 + if (task) { 48 + if (updates.priority) task.priority = updates.priority; 49 + if (updates.status) task.status = updates.status; 50 + return true; 51 + } 52 + return false; 49 53 } 50 54 51 55 clearTasks(): void { ··· 69 73 const priorityCounts = new Map<string, number>(); 70 74 const priorities = ['low', 'medium', 'high', 'critical']; 71 75 72 - // Initialize all priorities with 0 76 + // Initialize all priorities with 0 count 73 77 priorities.forEach(priority => priorityCounts.set(priority, 0)); 74 78 75 79 // Count tasks by priority ··· 94 98 const statusCounts = new Map<string, number>(); 95 99 const statuses = ['pending', 'in-progress', 'completed', 'blocked', 'cancelled']; 96 100 97 - // Initialize all statuses with 0 101 + // Initialize all statuses with 0 count 98 102 statuses.forEach(status => statusCounts.set(status, 0)); 99 103 100 104 // Count tasks by status ··· 115 119 }); 116 120 } 117 121 118 - getTasks(): readonly Task[] { 119 - return [...this.tasks]; 120 - } 121 - 122 - getTaskCount(): number { 123 - return this.tasks.length; 124 - } 125 - 126 122 getTasksByPriority(priority: Task['priority']): Task[] { 127 123 return this.tasks.filter(task => task.priority === priority); 128 124 } 129 125 130 126 getTasksByStatus(status: Task['status']): Task[] { 131 127 return this.tasks.filter(task => task.status === status); 128 + } 129 + 130 + getTotalTaskCount(): number { 131 + return this.tasks.length; 132 132 } 133 133 } 134 134 ··· 138 138 breakdown.addTasks(initialTasks); 139 139 } 140 140 return breakdown; 141 - } 142 - 143 - export function generateQuickReport(tasks: Task[]): BreakdownReport { 144 - const breakdown = createPriorityBreakdown(tasks); 145 - return breakdown.generateReport(); 146 141 } 147 142 148 143 /** @internal Phoenix VCS traceability — do not remove. */
+4 -10
examples/taskflow/src/generated/analytics/team-performance.ts
··· 40 40 totalAssigned: stats.total, 41 41 completed: stats.completed, 42 42 completionRate: stats.total > 0 ? stats.completed / stats.total : 0 43 - })) 44 - .sort((a, b) => b.completionRate - a.completionRate); 43 + })); 45 44 46 - const topPerformer = this.identifyTopPerformer(assigneePerformance); 45 + const topPerformer = this.findTopPerformer(assigneePerformance); 47 46 48 47 return { 49 48 assigneePerformance, ··· 51 50 }; 52 51 } 53 52 54 - private identifyTopPerformer(performance: AssigneePerformance[]): AssigneePerformance | null { 55 - const eligiblePerformers = performance.filter(p => p.totalAssigned >= 3); 53 + private findTopPerformer(performances: AssigneePerformance[]): AssigneePerformance | null { 54 + const eligiblePerformers = performances.filter(p => p.totalAssigned >= 3); 56 55 57 56 if (eligiblePerformers.length === 0) { 58 57 return null; ··· 83 82 export function getTopPerformer(tasks: Task[]): AssigneePerformance | null { 84 83 const metrics = calculateTeamPerformance(tasks); 85 84 return metrics.topPerformer; 86 - } 87 - 88 - export function getAssigneeCompletionRate(assignee: string, tasks: Task[]): number { 89 - const calculator = new TeamPerformanceCalculator(); 90 - return calculator.getCompletionRate(assignee, tasks); 91 85 } 92 86 93 87 /** @internal Phoenix VCS traceability — do not remove. */
+2
examples/taskflow/src/generated/index.ts
··· 6 6 7 7 export * as analytics from './analytics/index.js'; 8 8 export * as tasks from './tasks/index.js'; 9 + export * as webDashboard from './web-dashboard/index.js'; 9 10 10 11 export const services = [ 11 12 { name: 'Analytics', dir: 'analytics', port: 3000, modules: 3 }, 12 13 { name: 'Tasks', dir: 'tasks', port: 3001, modules: 4 }, 14 + { name: 'Web Dashboard', dir: 'web-dashboard', port: 3002, modules: 4 }, 13 15 ] as const;
+131 -15
examples/taskflow/src/generated/tasks/assignment.ts
··· 1 - /** 2 - * Assignment 3 - * 4 - * AUTO-GENERATED by Phoenix VCS — DO NOT EDIT DIRECTLY 5 - * Risk Tier: high 6 - */ 1 + export interface AssignmentRecord { 2 + taskId: string; 3 + assigneeId: string | null; 4 + assignedAt: Date; 5 + assignedBy: string; 6 + } 7 7 8 - /** Placeholder type — replace with your domain model. */ 9 - export type PreviousAssignee = Record<string, unknown>; 8 + export interface AssignmentAuditEntry { 9 + taskId: string; 10 + previousAssigneeId: string | null; 11 + newAssigneeId: string | null; 12 + changedAt: Date; 13 + changedBy: string; 14 + action: 'assigned' | 'reassigned' | 'unassigned'; 15 + } 10 16 11 - /** 12 - * Reassigning a task must log the previous assignee in an audit trail 13 - */ 14 - export function log(previousAssignee: PreviousAssignee): void { 15 - // TODO: implement 16 - throw new Error('Not implemented: log'); 17 + export interface Task { 18 + id: string; 19 + assigneeId: string | null; 20 + [key: string]: any; 21 + } 22 + 23 + export class AssignmentManager { 24 + private assignments = new Map<string, AssignmentRecord>(); 25 + private auditTrail: AssignmentAuditEntry[] = []; 26 + 27 + assignTask(taskId: string, assigneeId: string, assignedBy: string): void { 28 + if (!taskId.trim()) { 29 + throw new Error('Task ID cannot be empty'); 30 + } 31 + if (!assigneeId.trim()) { 32 + throw new Error('User ID cannot be empty'); 33 + } 34 + if (!assignedBy.trim()) { 35 + throw new Error('Assigned by user ID cannot be empty'); 36 + } 37 + 38 + const existingAssignment = this.assignments.get(taskId); 39 + const previousAssigneeId = existingAssignment?.assigneeId || null; 40 + 41 + const assignment: AssignmentRecord = { 42 + taskId, 43 + assigneeId, 44 + assignedAt: new Date(), 45 + assignedBy 46 + }; 47 + 48 + this.assignments.set(taskId, assignment); 49 + 50 + const auditEntry: AssignmentAuditEntry = { 51 + taskId, 52 + previousAssigneeId, 53 + newAssigneeId: assigneeId, 54 + changedAt: new Date(), 55 + changedBy: assignedBy, 56 + action: previousAssigneeId ? 'reassigned' : 'assigned' 57 + }; 58 + 59 + this.auditTrail.push(auditEntry); 60 + } 61 + 62 + unassignTask(taskId: string, unassignedBy: string): void { 63 + if (!taskId.trim()) { 64 + throw new Error('Task ID cannot be empty'); 65 + } 66 + if (!unassignedBy.trim()) { 67 + throw new Error('Unassigned by user ID cannot be empty'); 68 + } 69 + 70 + const existingAssignment = this.assignments.get(taskId); 71 + if (!existingAssignment) { 72 + throw new Error(`Task ${taskId} is not assigned`); 73 + } 74 + 75 + const previousAssigneeId = existingAssignment.assigneeId; 76 + this.assignments.delete(taskId); 77 + 78 + const auditEntry: AssignmentAuditEntry = { 79 + taskId, 80 + previousAssigneeId, 81 + newAssigneeId: null, 82 + changedAt: new Date(), 83 + changedBy: unassignedBy, 84 + action: 'unassigned' 85 + }; 86 + 87 + this.auditTrail.push(auditEntry); 88 + } 89 + 90 + getAssignment(taskId: string): AssignmentRecord | null { 91 + return this.assignments.get(taskId) || null; 92 + } 93 + 94 + getAssignedTasks(assigneeId: string): AssignmentRecord[] { 95 + if (!assigneeId.trim()) { 96 + throw new Error('User ID cannot be empty'); 97 + } 98 + 99 + return Array.from(this.assignments.values()).filter( 100 + assignment => assignment.assigneeId === assigneeId 101 + ); 102 + } 103 + 104 + getUnassignedTasks(allTasks: Task[]): Task[] { 105 + return allTasks.filter(task => !this.assignments.has(task.id)); 106 + } 107 + 108 + getAuditTrail(taskId?: string): AssignmentAuditEntry[] { 109 + if (taskId) { 110 + return this.auditTrail.filter(entry => entry.taskId === taskId); 111 + } 112 + return [...this.auditTrail]; 113 + } 114 + 115 + isTaskAssigned(taskId: string): boolean { 116 + return this.assignments.has(taskId); 117 + } 118 + 119 + getTaskAssignee(taskId: string): string | null { 120 + const assignment = this.assignments.get(taskId); 121 + return assignment?.assigneeId || null; 122 + } 123 + } 124 + 125 + export function validateUserId(userId: string): void { 126 + if (!userId || !userId.trim()) { 127 + throw new Error('User ID cannot be empty'); 128 + } 129 + } 130 + 131 + export function createAssignmentManager(): AssignmentManager { 132 + return new AssignmentManager(); 17 133 } 18 134 19 135 /** @internal Phoenix VCS traceability — do not remove. */ ··· 22 138 name: 'Assignment', 23 139 risk_tier: 'high', 24 140 canon_ids: [4 as const], 25 - } as const; 141 + } as const;
+89 -119
examples/taskflow/src/generated/tasks/deadline-management.ts
··· 1 - export interface TaskDeadline { 2 - taskId: string; 3 - deadline: Date; 4 - isOverdue: boolean; 5 - daysOverdue: number; 6 - } 7 - 8 1 export interface Task { 9 2 id: string; 10 3 title: string; 11 4 description?: string; 12 - deadline?: Date; 13 5 completed: boolean; 6 + deadline?: Date; 14 7 createdAt: Date; 15 - completedAt?: Date; 8 + updatedAt: Date; 16 9 } 17 10 18 11 export interface DeadlineWarning { 19 - message: string; 20 12 taskId: string; 21 - deadline: Date; 22 - severity: 'warning'; 13 + message: string; 14 + timestamp: Date; 15 + } 16 + 17 + export interface OverdueTask { 18 + task: Task; 19 + daysPastDeadline: number; 23 20 } 24 21 25 22 export class DeadlineManager { 26 23 private tasks = new Map<string, Task>(); 27 24 private warnings: DeadlineWarning[] = []; 28 25 29 - addTask(task: Task): void { 30 - if (task.deadline && this.isDateInPast(task.deadline)) { 31 - const warning: DeadlineWarning = { 32 - message: `Task "${task.title}" has a deadline in the past: ${task.deadline.toISOString()}`, 33 - taskId: task.id, 34 - deadline: task.deadline, 35 - severity: 'warning' 36 - }; 37 - this.warnings.push(warning); 38 - } 39 - 40 - this.tasks.set(task.id, { ...task }); 41 - } 42 - 43 - updateTask(taskId: string, updates: Partial<Task>): void { 26 + setTaskDeadline(taskId: string, deadline: Date): DeadlineWarning | null { 44 27 const task = this.tasks.get(taskId); 45 28 if (!task) { 46 29 throw new Error(`Task with id ${taskId} not found`); 47 30 } 48 31 49 - const updatedTask = { ...task, ...updates }; 32 + const now = new Date(); 33 + let warning: DeadlineWarning | null = null; 50 34 51 - if (updates.deadline && this.isDateInPast(updates.deadline)) { 52 - const warning: DeadlineWarning = { 53 - message: `Task "${updatedTask.title}" has a deadline in the past: ${updates.deadline.toISOString()}`, 54 - taskId: taskId, 55 - deadline: updates.deadline, 56 - severity: 'warning' 35 + if (deadline < now) { 36 + warning = { 37 + taskId, 38 + message: `Warning: Deadline set in the past (${deadline.toISOString()})`, 39 + timestamp: now 57 40 }; 58 41 this.warnings.push(warning); 59 42 } 60 43 61 - this.tasks.set(taskId, updatedTask); 44 + task.deadline = deadline; 45 + task.updatedAt = now; 46 + 47 + return warning; 62 48 } 63 49 64 - setDeadline(taskId: string, deadline: Date): void { 65 - const task = this.tasks.get(taskId); 66 - if (!task) { 67 - throw new Error(`Task with id ${taskId} not found`); 68 - } 50 + addTask(task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Task { 51 + const now = new Date(); 52 + const newTask: Task = { 53 + ...task, 54 + id: this.generateId(), 55 + createdAt: now, 56 + updatedAt: now 57 + }; 69 58 70 - if (this.isDateInPast(deadline)) { 59 + if (newTask.deadline && newTask.deadline < now) { 71 60 const warning: DeadlineWarning = { 72 - message: `Task "${task.title}" has a deadline in the past: ${deadline.toISOString()}`, 73 - taskId: taskId, 74 - deadline: deadline, 75 - severity: 'warning' 61 + taskId: newTask.id, 62 + message: `Warning: Task created with deadline in the past (${newTask.deadline.toISOString()})`, 63 + timestamp: now 76 64 }; 77 65 this.warnings.push(warning); 78 66 } 79 67 80 - this.tasks.set(taskId, { ...task, deadline }); 68 + this.tasks.set(newTask.id, newTask); 69 + return newTask; 81 70 } 82 71 83 - removeDeadline(taskId: string): void { 72 + updateTask(taskId: string, updates: Partial<Omit<Task, 'id' | 'createdAt' | 'updatedAt'>>): Task { 84 73 const task = this.tasks.get(taskId); 85 74 if (!task) { 86 75 throw new Error(`Task with id ${taskId} not found`); 87 76 } 88 77 89 - const updatedTask = { ...task }; 90 - delete updatedTask.deadline; 91 - this.tasks.set(taskId, updatedTask); 92 - } 93 - 94 - getOverdueTasks(): TaskDeadline[] { 95 78 const now = new Date(); 96 - const overdueTasks: TaskDeadline[] = []; 97 - 98 - for (const task of this.tasks.values()) { 99 - if (task.deadline && !task.completed && task.deadline < now) { 100 - const daysOverdue = Math.ceil((now.getTime() - task.deadline.getTime()) / (1000 * 60 * 60 * 24)); 101 - 102 - overdueTasks.push({ 103 - taskId: task.id, 104 - deadline: task.deadline, 105 - isOverdue: true, 106 - daysOverdue 107 - }); 108 - } 109 - } 110 - 111 - return overdueTasks.sort((a, b) => b.daysOverdue - a.daysOverdue); 112 - } 113 - 114 - getTasksWithDeadlines(): TaskDeadline[] { 115 - const now = new Date(); 116 - const tasksWithDeadlines: TaskDeadline[] = []; 117 - 118 - for (const task of this.tasks.values()) { 119 - if (task.deadline) { 120 - const isOverdue = !task.completed && task.deadline < now; 121 - const daysOverdue = isOverdue 122 - ? Math.ceil((now.getTime() - task.deadline.getTime()) / (1000 * 60 * 60 * 24)) 123 - : 0; 79 + const updatedTask: Task = { 80 + ...task, 81 + ...updates, 82 + updatedAt: now 83 + }; 124 84 125 - tasksWithDeadlines.push({ 126 - taskId: task.id, 127 - deadline: task.deadline, 128 - isOverdue, 129 - daysOverdue 130 - }); 131 - } 85 + if (updates.deadline && updates.deadline < now && !task.completed) { 86 + const warning: DeadlineWarning = { 87 + taskId, 88 + message: `Warning: Deadline updated to past date (${updates.deadline.toISOString()})`, 89 + timestamp: now 90 + }; 91 + this.warnings.push(warning); 132 92 } 133 93 134 - return tasksWithDeadlines.sort((a, b) => a.deadline.getTime() - b.deadline.getTime()); 94 + this.tasks.set(taskId, updatedTask); 95 + return updatedTask; 135 96 } 136 97 137 98 getTask(taskId: string): Task | undefined { ··· 142 103 return Array.from(this.tasks.values()); 143 104 } 144 105 145 - getWarnings(): DeadlineWarning[] { 146 - return [...this.warnings]; 147 - } 106 + getOverdueTasks(): OverdueTask[] { 107 + const now = new Date(); 108 + const overdueTasks: OverdueTask[] = []; 148 109 149 - clearWarnings(): void { 150 - this.warnings = []; 110 + for (const task of this.tasks.values()) { 111 + if (task.deadline && !task.completed && task.deadline < now) { 112 + const daysPastDeadline = Math.floor( 113 + (now.getTime() - task.deadline.getTime()) / (1000 * 60 * 60 * 24) 114 + ); 115 + overdueTasks.push({ 116 + task, 117 + daysPastDeadline 118 + }); 119 + } 120 + } 121 + 122 + return overdueTasks.sort((a, b) => b.daysPastDeadline - a.daysPastDeadline); 151 123 } 152 124 153 125 isTaskOverdue(taskId: string): boolean { ··· 155 127 if (!task || !task.deadline || task.completed) { 156 128 return false; 157 129 } 158 - 159 130 return task.deadline < new Date(); 160 131 } 161 132 162 - private isDateInPast(date: Date): boolean { 163 - const now = new Date(); 164 - return date < now; 133 + getWarnings(): DeadlineWarning[] { 134 + return [...this.warnings]; 135 + } 136 + 137 + clearWarnings(): void { 138 + this.warnings = []; 139 + } 140 + 141 + private generateId(): string { 142 + return Math.random().toString(36).substring(2) + Date.now().toString(36); 165 143 } 166 144 } 167 145 ··· 169 147 return new DeadlineManager(); 170 148 } 171 149 172 - export function isOverdue(deadline: Date, completed: boolean): boolean { 173 - if (completed) { 174 - return false; 175 - } 176 - return deadline < new Date(); 177 - } 178 - 179 - export function calculateDaysOverdue(deadline: Date): number { 150 + export function calculateDaysUntilDeadline(deadline: Date): number { 180 151 const now = new Date(); 181 - if (deadline >= now) { 182 - return 0; 183 - } 184 - return Math.ceil((now.getTime() - deadline.getTime()) / (1000 * 60 * 60 * 24)); 152 + const diffTime = deadline.getTime() - now.getTime(); 153 + return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); 185 154 } 186 155 187 - export function formatDeadlineStatus(deadline: Date, completed: boolean): string { 188 - if (completed) { 189 - return 'Completed'; 156 + export function formatDeadlineStatus(task: Task): string { 157 + if (!task.deadline) { 158 + return 'No deadline'; 190 159 } 191 160 192 - const now = new Date(); 193 - if (deadline < now) { 194 - const daysOverdue = calculateDaysOverdue(deadline); 195 - return `Overdue by ${daysOverdue} day${daysOverdue === 1 ? '' : 's'}`; 161 + if (task.completed) { 162 + return 'Completed'; 196 163 } 197 164 198 - const daysUntil = Math.ceil((deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); 199 - if (daysUntil === 0) { 165 + const daysUntil = calculateDaysUntilDeadline(task.deadline); 166 + 167 + if (daysUntil < 0) { 168 + return `Overdue by ${Math.abs(daysUntil)} day${Math.abs(daysUntil) === 1 ? '' : 's'}`; 169 + } else if (daysUntil === 0) { 200 170 return 'Due today'; 201 171 } else if (daysUntil === 1) { 202 172 return 'Due tomorrow';
+17 -16
examples/taskflow/src/generated/tasks/search-and-filtering.ts
··· 3 3 title: string; 4 4 priority: 'critical' | 'high' | 'medium' | 'low'; 5 5 created_at: Date; 6 + status: string; 6 7 description?: string; 7 - status?: string; 8 8 assignee?: string; 9 9 deadline?: Date; 10 10 } ··· 31 31 export class TaskSearchEngine { 32 32 private tasks: Task[] = []; 33 33 34 - constructor(initialTasks: Task[] = []) { 35 - this.tasks = [...initialTasks]; 34 + constructor(tasks: Task[] = []) { 35 + this.tasks = [...tasks]; 36 36 } 37 37 38 38 addTask(task: Task): void { ··· 44 44 } 45 45 46 46 removeTask(taskId: string): boolean { 47 - const initialLength = this.tasks.length; 48 - this.tasks = this.tasks.filter(task => task.id !== taskId); 49 - return this.tasks.length < initialLength; 47 + const index = this.tasks.findIndex(task => task.id === taskId); 48 + if (index !== -1) { 49 + this.tasks.splice(index, 1); 50 + return true; 51 + } 52 + return false; 50 53 } 51 54 52 55 updateTask(taskId: string, updates: Partial<Task>): boolean { 53 - const taskIndex = this.tasks.findIndex(task => task.id === taskId); 54 - if (taskIndex === -1) { 55 - return false; 56 + const index = this.tasks.findIndex(task => task.id === taskId); 57 + if (index !== -1) { 58 + this.tasks[index] = { ...this.tasks[index], ...updates }; 59 + return true; 56 60 } 57 - this.tasks[taskIndex] = { ...this.tasks[taskIndex], ...updates }; 58 - return true; 61 + return false; 59 62 } 60 63 61 64 search(options: SearchOptions = {}): SearchResult { ··· 124 127 return [...tasks]; 125 128 } 126 129 127 - const searchTerm = titleSubstring.toLowerCase().trim(); 130 + const searchQuery = titleSubstring.toLowerCase().trim(); 128 131 return tasks.filter(task => 129 - task.title.toLowerCase().includes(searchTerm) 132 + task.title.toLowerCase().includes(searchQuery) 130 133 ); 131 134 } 132 135 133 - export function sortTasksByPriorityAndDate(tasks: Task[]): Task[] { 136 + export function sortTasksByPriority(tasks: Task[]): Task[] { 134 137 return [...tasks].sort((a, b) => { 135 - // Critical first, then by priority order 136 138 const priorityComparison = PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]; 137 139 138 - // If priorities are equal, sort by created_at 139 140 if (priorityComparison === 0) { 140 141 return a.created_at.getTime() - b.created_at.getTime(); 141 142 }
+85 -91
examples/taskflow/src/generated/tasks/task-lifecycle.ts
··· 6 6 export interface Task { 7 7 id: string; 8 8 title: string; 9 - description: string; 9 + description?: string; 10 + status: TaskStatus; 10 11 priority: TaskPriority; 11 - status: TaskStatus; 12 - assignee?: string; 13 12 created_at: Date; 14 13 updated_at: Date; 15 14 completed_at?: Date; 16 15 duration_ms?: number; 17 16 } 18 17 19 - export interface CreateTaskInput { 18 + export interface TaskCreateInput { 20 19 title: string; 21 - description: string; 20 + description?: string; 22 21 priority: TaskPriority; 23 - assignee?: string; 24 22 } 25 23 26 - export interface TaskFilter { 27 - status?: TaskStatus; 24 + export interface TaskUpdateInput { 25 + title?: string; 26 + description?: string; 28 27 priority?: TaskPriority; 29 - assignee?: string; 28 + } 29 + 30 + export class TaskLifecycleError extends Error { 31 + constructor(message: string) { 32 + super(message); 33 + this.name = 'TaskLifecycleError'; 34 + } 30 35 } 31 36 32 37 const VALID_TRANSITIONS: Record<TaskStatus, TaskStatus[]> = { ··· 36 41 done: [] 37 42 }; 38 43 39 - export class TaskLifecycleManager { 44 + export class TaskLifecycle { 40 45 private tasks = new Map<string, Task>(); 41 46 42 - createTask(input: CreateTaskInput): Task { 43 - if (!input.title.trim()) { 44 - throw new Error('Task title cannot be empty'); 45 - } 46 - if (!input.description.trim()) { 47 - throw new Error('Task description cannot be empty'); 48 - } 49 - if (!this.isValidPriority(input.priority)) { 50 - throw new Error(`Invalid priority: ${input.priority}. Must be one of: low, medium, high, critical`); 51 - } 52 - 47 + createTask(input: TaskCreateInput): Task { 53 48 const now = new Date(); 54 49 const task: Task = { 55 50 id: randomUUID(), 56 - title: input.title.trim(), 57 - description: input.description.trim(), 51 + title: input.title, 52 + description: input.description, 53 + status: 'open', 58 54 priority: input.priority, 59 - status: 'open', 60 - assignee: input.assignee?.trim(), 61 55 created_at: now, 62 56 updated_at: now 63 57 }; ··· 71 65 return task ? { ...task } : undefined; 72 66 } 73 67 74 - updateTaskStatus(id: string, newStatus: TaskStatus): Task { 68 + getAllTasks(): Task[] { 69 + return Array.from(this.tasks.values()).map(task => ({ ...task })); 70 + } 71 + 72 + updateTask(id: string, input: TaskUpdateInput): Task { 75 73 const task = this.tasks.get(id); 76 74 if (!task) { 77 - throw new Error(`Task not found: ${id}`); 78 - } 79 - 80 - if (!this.isValidStatusTransition(task.status, newStatus)) { 81 - throw new Error(`Invalid status transition from ${task.status} to ${newStatus}`); 75 + throw new TaskLifecycleError(`Task with id ${id} not found`); 82 76 } 83 77 84 - const now = new Date(); 85 - task.status = newStatus; 86 - task.updated_at = now; 87 - 88 - if (newStatus === 'done' && !task.completed_at) { 89 - task.completed_at = now; 90 - task.duration_ms = now.getTime() - task.created_at.getTime(); 91 - } 78 + const updatedTask: Task = { 79 + ...task, 80 + ...input, 81 + updated_at: new Date() 82 + }; 92 83 93 - return { ...task }; 84 + this.tasks.set(id, updatedTask); 85 + return { ...updatedTask }; 94 86 } 95 87 96 - updateTask(id: string, updates: Partial<Pick<Task, 'title' | 'description' | 'priority' | 'assignee'>>): Task { 88 + transitionStatus(id: string, newStatus: TaskStatus): Task { 97 89 const task = this.tasks.get(id); 98 90 if (!task) { 99 - throw new Error(`Task not found: ${id}`); 100 - } 101 - 102 - if (updates.title !== undefined) { 103 - if (!updates.title.trim()) { 104 - throw new Error('Task title cannot be empty'); 105 - } 106 - task.title = updates.title.trim(); 91 + throw new TaskLifecycleError(`Task with id ${id} not found`); 107 92 } 108 93 109 - if (updates.description !== undefined) { 110 - if (!updates.description.trim()) { 111 - throw new Error('Task description cannot be empty'); 112 - } 113 - task.description = updates.description.trim(); 94 + const validTransitions = VALID_TRANSITIONS[task.status]; 95 + if (!validTransitions.includes(newStatus)) { 96 + throw new TaskLifecycleError( 97 + `Invalid status transition from ${task.status} to ${newStatus}. Valid transitions: ${validTransitions.join(', ')}` 98 + ); 114 99 } 115 100 116 - if (updates.priority !== undefined) { 117 - if (!this.isValidPriority(updates.priority)) { 118 - throw new Error(`Invalid priority: ${updates.priority}. Must be one of: low, medium, high, critical`); 119 - } 120 - task.priority = updates.priority; 121 - } 101 + const now = new Date(); 102 + const updatedTask: Task = { 103 + ...task, 104 + status: newStatus, 105 + updated_at: now 106 + }; 122 107 123 - if (updates.assignee !== undefined) { 124 - task.assignee = updates.assignee?.trim(); 108 + if (newStatus === 'done') { 109 + updatedTask.completed_at = now; 110 + updatedTask.duration_ms = now.getTime() - task.created_at.getTime(); 125 111 } 126 112 127 - task.updated_at = new Date(); 128 - return { ...task }; 113 + this.tasks.set(id, updatedTask); 114 + return { ...updatedTask }; 129 115 } 130 116 131 - filterTasks(filter: TaskFilter): Task[] { 132 - const results: Task[] = []; 133 - 134 - for (const task of this.tasks.values()) { 135 - if (filter.status && task.status !== filter.status) { 136 - continue; 137 - } 138 - if (filter.priority && task.priority !== filter.priority) { 139 - continue; 140 - } 141 - if (filter.assignee && task.assignee !== filter.assignee) { 142 - continue; 143 - } 144 - results.push({ ...task }); 145 - } 117 + deleteTask(id: string): boolean { 118 + return this.tasks.delete(id); 119 + } 146 120 147 - return results; 121 + getTasksByStatus(status: TaskStatus): Task[] { 122 + return Array.from(this.tasks.values()) 123 + .filter(task => task.status === status) 124 + .map(task => ({ ...task })); 148 125 } 149 126 150 - getAllTasks(): Task[] { 151 - return Array.from(this.tasks.values()).map(task => ({ ...task })); 127 + getTasksByPriority(priority: TaskPriority): Task[] { 128 + return Array.from(this.tasks.values()) 129 + .filter(task => task.priority === priority) 130 + .map(task => ({ ...task })); 152 131 } 153 132 154 - deleteTask(id: string): boolean { 155 - return this.tasks.delete(id); 133 + getCompletedTasks(): Task[] { 134 + return Array.from(this.tasks.values()) 135 + .filter(task => task.status === 'done' && task.completed_at) 136 + .map(task => ({ ...task })); 156 137 } 157 138 158 - private isValidPriority(priority: string): priority is TaskPriority { 159 - return ['low', 'medium', 'high', 'critical'].includes(priority); 139 + getTaskDuration(id: string): number | undefined { 140 + const task = this.tasks.get(id); 141 + return task?.duration_ms; 160 142 } 161 143 162 - private isValidStatusTransition(currentStatus: TaskStatus, newStatus: TaskStatus): boolean { 144 + isValidTransition(currentStatus: TaskStatus, newStatus: TaskStatus): boolean { 163 145 return VALID_TRANSITIONS[currentStatus].includes(newStatus); 146 + } 147 + 148 + getValidTransitions(status: TaskStatus): TaskStatus[] { 149 + return [...VALID_TRANSITIONS[status]]; 164 150 } 165 151 } 166 152 167 - export function createTaskLifecycleManager(): TaskLifecycleManager { 168 - return new TaskLifecycleManager(); 153 + export function createTaskLifecycle(): TaskLifecycle { 154 + return new TaskLifecycle(); 155 + } 156 + 157 + export function validateTaskStatus(status: string): status is TaskStatus { 158 + return ['open', 'in_progress', 'review', 'done'].includes(status); 159 + } 160 + 161 + export function validateTaskPriority(priority: string): priority is TaskPriority { 162 + return ['low', 'medium', 'high', 'critical'].includes(priority); 169 163 } 170 164 171 165 /** @internal Phoenix VCS traceability — do not remove. */ 172 166 export const _phoenix = { 173 - iu_id: 'ff644f70786609347751a6f701a6ea069ff8a8bb01eb48caa2a15a43b97ce081', 167 + iu_id: 'aa5572d4c3285803b48ca24b2d436cc3f2ad7489a5bf1e933067932d6d7c8264', 174 168 name: 'Task Lifecycle', 175 169 risk_tier: 'high', 176 - canon_ids: [8 as const], 170 + canon_ids: [6 as const], 177 171 } as const;
+117
examples/taskflow/src/generated/web-dashboard/__tests__/web-dashboard.test.ts
··· 1 + /** 2 + * Web Dashboard — Generated Tests 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Tests module structure, server health, and Phoenix traceability. 6 + */ 7 + 8 + import { describe, it, expect, afterAll } from 'vitest'; 9 + import { startServer } from '../server.js'; 10 + 11 + import * as analyticsPanel from '../analytics-panel.js'; 12 + import * as dashboardPage from '../dashboard-page.js'; 13 + import * as styles from '../styles.js'; 14 + import * as taskListDisplay from '../task-list-display.js'; 15 + 16 + describe('Web Dashboard modules', () => { 17 + describe('Analytics Panel', () => { 18 + it('exports Phoenix traceability metadata', () => { 19 + expect(analyticsPanel._phoenix).toBeDefined(); 20 + expect(analyticsPanel._phoenix.name).toBe('Analytics Panel'); 21 + expect(analyticsPanel._phoenix.risk_tier).toBeTruthy(); 22 + }); 23 + 24 + it('has exported functions', () => { 25 + const exports = Object.keys(analyticsPanel).filter(k => k !== '_phoenix'); 26 + expect(exports.length).toBeGreaterThan(0); 27 + }); 28 + }); 29 + 30 + describe('Dashboard Page', () => { 31 + it('exports Phoenix traceability metadata', () => { 32 + expect(dashboardPage._phoenix).toBeDefined(); 33 + expect(dashboardPage._phoenix.name).toBe('Dashboard Page'); 34 + expect(dashboardPage._phoenix.risk_tier).toBeTruthy(); 35 + }); 36 + 37 + it('has exported functions', () => { 38 + const exports = Object.keys(dashboardPage).filter(k => k !== '_phoenix'); 39 + expect(exports.length).toBeGreaterThan(0); 40 + }); 41 + }); 42 + 43 + describe('Styles', () => { 44 + it('exports Phoenix traceability metadata', () => { 45 + expect(styles._phoenix).toBeDefined(); 46 + expect(styles._phoenix.name).toBe('Styles'); 47 + expect(styles._phoenix.risk_tier).toBeTruthy(); 48 + }); 49 + 50 + it('has exported functions', () => { 51 + const exports = Object.keys(styles).filter(k => k !== '_phoenix'); 52 + expect(exports.length).toBeGreaterThan(0); 53 + }); 54 + }); 55 + 56 + describe('Task List Display', () => { 57 + it('exports Phoenix traceability metadata', () => { 58 + expect(taskListDisplay._phoenix).toBeDefined(); 59 + expect(taskListDisplay._phoenix.name).toBe('Task List Display'); 60 + expect(taskListDisplay._phoenix.risk_tier).toBeTruthy(); 61 + }); 62 + 63 + it('has exported functions', () => { 64 + const exports = Object.keys(taskListDisplay).filter(k => k !== '_phoenix'); 65 + expect(exports.length).toBeGreaterThan(0); 66 + }); 67 + }); 68 + 69 + }); 70 + 71 + describe('Web Dashboard server', () => { 72 + const instance = startServer(0); // random port 73 + 74 + afterAll(() => new Promise<void>(resolve => instance.server.close(() => resolve()))); 75 + 76 + it('GET /health returns 200', async () => { 77 + await instance.ready; 78 + const res = await fetch(`http://localhost:${instance.port}/health`); 79 + expect(res.status).toBe(200); 80 + const body = await res.json() as Record<string, unknown>; 81 + expect(body.status).toBe('ok'); 82 + expect(body.service).toBe('Web Dashboard'); 83 + }); 84 + 85 + it('GET /metrics returns request counts', async () => { 86 + await instance.ready; 87 + const res = await fetch(`http://localhost:${instance.port}/metrics`); 88 + expect(res.status).toBe(200); 89 + const body = await res.json() as Record<string, unknown>; 90 + expect(typeof body.requests_total).toBe('number'); 91 + }); 92 + 93 + it('GET /modules lists all registered modules', async () => { 94 + await instance.ready; 95 + const res = await fetch(`http://localhost:${instance.port}/modules`); 96 + expect(res.status).toBe(200); 97 + const body = await res.json() as Array<Record<string, unknown>>; 98 + expect(body.length).toBe(4); 99 + }); 100 + 101 + it('GET /unknown returns 404', async () => { 102 + await instance.ready; 103 + const res = await fetch(`http://localhost:${instance.port}/unknown`); 104 + expect(res.status).toBe(404); 105 + }); 106 + 107 + it('GET / serves HTML page', async () => { 108 + await instance.ready; 109 + const res = await fetch(`http://localhost:${instance.port}/`); 110 + expect(res.status).toBe(200); 111 + const ct = res.headers.get('content-type') ?? ''; 112 + expect(ct).toContain('text/html'); 113 + const body = await res.text(); 114 + expect(body).toContain('<!DOCTYPE html>'); 115 + expect(body).toContain('<title>Web Dashboard</title>'); 116 + }); 117 + });
+121
examples/taskflow/src/generated/web-dashboard/analytics-panel.ts
··· 1 + export interface TaskStats { 2 + totalTasks: number; 3 + completedCount: number; 4 + overdueCount: number; 5 + completionRate: number; 6 + } 7 + 8 + export interface MetricCard { 9 + name: string; 10 + value: string; 11 + emoji: string; 12 + } 13 + 14 + export interface AnalyticsPanelOptions { 15 + stats: TaskStats; 16 + className?: string; 17 + } 18 + 19 + export function calculateCompletionRate(completed: number, total: number): number { 20 + if (total === 0) return 0; 21 + return Math.round((completed / total) * 100); 22 + } 23 + 24 + export function formatMetricValue(value: number, type: 'count' | 'percentage'): string { 25 + if (type === 'percentage') { 26 + return `${value}%`; 27 + } 28 + return value.toString(); 29 + } 30 + 31 + export function createMetricCards(stats: TaskStats): MetricCard[] { 32 + return [ 33 + { 34 + name: 'Total Tasks', 35 + value: formatMetricValue(stats.totalTasks, 'count'), 36 + emoji: '📋' 37 + }, 38 + { 39 + name: 'Completed', 40 + value: formatMetricValue(stats.completedCount, 'count'), 41 + emoji: '✅' 42 + }, 43 + { 44 + name: 'Overdue', 45 + value: formatMetricValue(stats.overdueCount, 'count'), 46 + emoji: '⚠️' 47 + }, 48 + { 49 + name: 'Completion Rate', 50 + value: formatMetricValue(stats.completionRate, 'percentage'), 51 + emoji: '📊' 52 + } 53 + ]; 54 + } 55 + 56 + export function renderMetricCard(card: MetricCard): string { 57 + return ` 58 + <div class="metric-card"> 59 + <div class="metric-icon">${card.emoji}</div> 60 + <div class="metric-content"> 61 + <div class="metric-name">${card.name}</div> 62 + <div class="metric-value">${card.value}</div> 63 + </div> 64 + </div> 65 + `.trim(); 66 + } 67 + 68 + export function renderAnalyticsPanel(options: AnalyticsPanelOptions): string { 69 + const { stats, className = 'analytics-panel' } = options; 70 + const cards = createMetricCards(stats); 71 + const cardHtml = cards.map(renderMetricCard).join('\n'); 72 + 73 + return ` 74 + <div class="${className}"> 75 + <div class="metrics-row"> 76 + ${cardHtml} 77 + </div> 78 + </div> 79 + `.trim(); 80 + } 81 + 82 + export class AnalyticsPanel { 83 + private stats: TaskStats; 84 + private className: string; 85 + 86 + constructor(stats: TaskStats, className = 'analytics-panel') { 87 + this.stats = stats; 88 + this.className = className; 89 + } 90 + 91 + updateStats(newStats: TaskStats): void { 92 + this.stats = { ...newStats }; 93 + this.stats.completionRate = calculateCompletionRate( 94 + newStats.completedCount, 95 + newStats.totalTasks 96 + ); 97 + } 98 + 99 + getStats(): TaskStats { 100 + return { ...this.stats }; 101 + } 102 + 103 + render(): string { 104 + return renderAnalyticsPanel({ 105 + stats: this.stats, 106 + className: this.className 107 + }); 108 + } 109 + 110 + getMetricCards(): MetricCard[] { 111 + return createMetricCards(this.stats); 112 + } 113 + } 114 + 115 + /** @internal Phoenix VCS traceability — do not remove. */ 116 + export const _phoenix = { 117 + iu_id: '59d32939d50083b6ecf70e500f215a179f42a5ea99a186cb8af2720e3aaa1d74', 118 + name: 'Analytics Panel', 119 + risk_tier: 'low', 120 + canon_ids: [3 as const], 121 + } as const;
+493
examples/taskflow/src/generated/web-dashboard/dashboard-page.ts
··· 1 + export interface Task { 2 + id: string; 3 + title: string; 4 + description: string; 5 + priority: 'low' | 'medium' | 'high'; 6 + deadline?: string; 7 + createdAt: string; 8 + } 9 + 10 + export interface TaskFormData { 11 + title: string; 12 + description: string; 13 + priority: 'low' | 'medium' | 'high'; 14 + deadline?: string; 15 + } 16 + 17 + export interface DashboardState { 18 + tasks: Task[]; 19 + taskCount: number; 20 + } 21 + 22 + export class DashboardPage { 23 + private tasks: Task[] = []; 24 + 25 + public addTask(formData: TaskFormData): Task { 26 + this.validateTaskForm(formData); 27 + 28 + const task: Task = { 29 + id: this.generateId(), 30 + title: formData.title.trim(), 31 + description: formData.description.trim(), 32 + priority: formData.priority, 33 + deadline: formData.deadline || undefined, 34 + createdAt: new Date().toISOString(), 35 + }; 36 + 37 + this.tasks.push(task); 38 + return task; 39 + } 40 + 41 + public getTasks(): Task[] { 42 + return [...this.tasks]; 43 + } 44 + 45 + public getTaskCount(): number { 46 + return this.tasks.length; 47 + } 48 + 49 + public renderHTML(): string { 50 + const taskCount = this.getTaskCount(); 51 + 52 + return `<!DOCTYPE html> 53 + <html lang="en"> 54 + <head> 55 + <meta charset="UTF-8"> 56 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 57 + <title>TaskFlow Dashboard</title> 58 + <style> 59 + :root { 60 + --primary: #2563eb; 61 + --danger: #dc2626; 62 + --success: #16a34a; 63 + --warning: #d97706; 64 + --primary-light: #dbeafe; 65 + --gray-50: #f9fafb; 66 + --gray-100: #f3f4f6; 67 + --gray-200: #e5e7eb; 68 + --gray-300: #d1d5db; 69 + --gray-600: #4b5563; 70 + --gray-800: #1f2937; 71 + --gray-900: #111827; 72 + } 73 + 74 + * { 75 + margin: 0; 76 + padding: 0; 77 + box-sizing: border-box; 78 + } 79 + 80 + body { 81 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 82 + background-color: var(--gray-50); 83 + color: var(--gray-900); 84 + line-height: 1.6; 85 + } 86 + 87 + .container { 88 + max-width: 1200px; 89 + margin: 0 auto; 90 + padding: 0 1rem; 91 + } 92 + 93 + .header { 94 + background: white; 95 + border-bottom: 1px solid var(--gray-200); 96 + padding: 1rem 0; 97 + margin-bottom: 2rem; 98 + } 99 + 100 + .header-content { 101 + display: flex; 102 + justify-content: space-between; 103 + align-items: center; 104 + } 105 + 106 + .logo { 107 + font-size: 1.75rem; 108 + font-weight: 700; 109 + color: var(--primary); 110 + } 111 + 112 + .task-summary { 113 + background: var(--primary-light); 114 + padding: 0.5rem 1rem; 115 + border-radius: 0.5rem; 116 + font-weight: 600; 117 + color: var(--primary); 118 + } 119 + 120 + .main-content { 121 + display: grid; 122 + grid-template-columns: 1fr 2fr; 123 + gap: 2rem; 124 + margin-bottom: 2rem; 125 + } 126 + 127 + .card { 128 + background: white; 129 + border-radius: 0.75rem; 130 + padding: 1.5rem; 131 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 132 + } 133 + 134 + .card-title { 135 + font-size: 1.25rem; 136 + font-weight: 600; 137 + margin-bottom: 1.5rem; 138 + color: var(--gray-800); 139 + } 140 + 141 + .form-group { 142 + margin-bottom: 1rem; 143 + } 144 + 145 + .form-label { 146 + display: block; 147 + font-weight: 500; 148 + margin-bottom: 0.5rem; 149 + color: var(--gray-700); 150 + } 151 + 152 + .form-input, 153 + .form-select, 154 + .form-textarea { 155 + width: 100%; 156 + padding: 0.75rem; 157 + border: 1px solid var(--gray-300); 158 + border-radius: 0.5rem; 159 + font-size: 0.875rem; 160 + transition: border-color 0.2s; 161 + } 162 + 163 + .form-input:focus, 164 + .form-select:focus, 165 + .form-textarea:focus { 166 + outline: none; 167 + border-color: var(--primary); 168 + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); 169 + } 170 + 171 + .form-textarea { 172 + resize: vertical; 173 + min-height: 80px; 174 + } 175 + 176 + .btn { 177 + display: inline-flex; 178 + align-items: center; 179 + justify-content: center; 180 + padding: 0.75rem 1.5rem; 181 + border: none; 182 + border-radius: 0.5rem; 183 + font-weight: 500; 184 + cursor: pointer; 185 + transition: all 0.2s; 186 + text-decoration: none; 187 + } 188 + 189 + .btn-primary { 190 + background: var(--primary); 191 + color: white; 192 + } 193 + 194 + .btn-primary:hover { 195 + background: #1d4ed8; 196 + } 197 + 198 + .btn-primary:disabled { 199 + background: var(--gray-300); 200 + cursor: not-allowed; 201 + } 202 + 203 + .error-message { 204 + color: var(--danger); 205 + font-size: 0.875rem; 206 + margin-top: 0.25rem; 207 + display: none; 208 + } 209 + 210 + .error-message.show { 211 + display: block; 212 + } 213 + 214 + .priority-badge { 215 + display: inline-block; 216 + padding: 0.25rem 0.5rem; 217 + border-radius: 0.25rem; 218 + font-size: 0.75rem; 219 + font-weight: 500; 220 + text-transform: uppercase; 221 + } 222 + 223 + .priority-low { 224 + background: var(--success); 225 + color: white; 226 + } 227 + 228 + .priority-medium { 229 + background: var(--warning); 230 + color: white; 231 + } 232 + 233 + .priority-high { 234 + background: var(--danger); 235 + color: white; 236 + } 237 + 238 + .task-list { 239 + display: flex; 240 + flex-direction: column; 241 + gap: 1rem; 242 + } 243 + 244 + .task-item { 245 + padding: 1rem; 246 + border: 1px solid var(--gray-200); 247 + border-radius: 0.5rem; 248 + background: var(--gray-50); 249 + } 250 + 251 + .task-header { 252 + display: flex; 253 + justify-content: space-between; 254 + align-items: flex-start; 255 + margin-bottom: 0.5rem; 256 + } 257 + 258 + .task-title { 259 + font-weight: 600; 260 + color: var(--gray-800); 261 + } 262 + 263 + .task-description { 264 + color: var(--gray-600); 265 + margin-bottom: 0.5rem; 266 + } 267 + 268 + .task-meta { 269 + display: flex; 270 + gap: 1rem; 271 + font-size: 0.875rem; 272 + color: var(--gray-500); 273 + } 274 + 275 + @media (max-width: 768px) { 276 + .main-content { 277 + grid-template-columns: 1fr; 278 + } 279 + 280 + .header-content { 281 + flex-direction: column; 282 + gap: 1rem; 283 + } 284 + } 285 + </style> 286 + </head> 287 + <body> 288 + <header class="header"> 289 + <div class="container"> 290 + <div class="header-content"> 291 + <h1 class="logo">TaskFlow</h1> 292 + <div class="task-summary"> 293 + <span id="task-count">${taskCount}</span> tasks 294 + </div> 295 + </div> 296 + </div> 297 + </header> 298 + 299 + <div class="container"> 300 + <div class="main-content"> 301 + <div class="card"> 302 + <h2 class="card-title">Create New Task</h2> 303 + <form id="task-form"> 304 + <div class="form-group"> 305 + <label class="form-label" for="title">Title *</label> 306 + <input type="text" id="title" name="title" class="form-input" required> 307 + <div class="error-message" id="title-error">Title is required</div> 308 + </div> 309 + 310 + <div class="form-group"> 311 + <label class="form-label" for="description">Description</label> 312 + <textarea id="description" name="description" class="form-textarea" rows="3"></textarea> 313 + </div> 314 + 315 + <div class="form-group"> 316 + <label class="form-label" for="priority">Priority</label> 317 + <select id="priority" name="priority" class="form-select"> 318 + <option value="low">Low</option> 319 + <option value="medium" selected>Medium</option> 320 + <option value="high">High</option> 321 + </select> 322 + </div> 323 + 324 + <div class="form-group"> 325 + <label class="form-label" for="deadline">Deadline (Optional)</label> 326 + <input type="date" id="deadline" name="deadline" class="form-input"> 327 + </div> 328 + 329 + <button type="submit" class="btn btn-primary">Create Task</button> 330 + </form> 331 + </div> 332 + 333 + <div class="card"> 334 + <h2 class="card-title">Recent Tasks</h2> 335 + <div class="task-list" id="task-list"> 336 + ${this.renderTaskList()} 337 + </div> 338 + </div> 339 + </div> 340 + </div> 341 + 342 + <script> 343 + (function() { 344 + const form = document.getElementById('task-form'); 345 + const titleInput = document.getElementById('title'); 346 + const titleError = document.getElementById('title-error'); 347 + const taskCountEl = document.getElementById('task-count'); 348 + const taskList = document.getElementById('task-list'); 349 + 350 + function validateTitle() { 351 + const title = titleInput.value.trim(); 352 + if (!title) { 353 + titleError.classList.add('show'); 354 + titleInput.style.borderColor = 'var(--danger)'; 355 + return false; 356 + } else { 357 + titleError.classList.remove('show'); 358 + titleInput.style.borderColor = ''; 359 + return true; 360 + } 361 + } 362 + 363 + function formatDate(dateString) { 364 + if (!dateString) return ''; 365 + return new Date(dateString).toLocaleDateString(); 366 + } 367 + 368 + function createTaskElement(task) { 369 + return \` 370 + <div class="task-item"> 371 + <div class="task-header"> 372 + <h3 class="task-title">\${task.title}</h3> 373 + <span class="priority-badge priority-\${task.priority}">\${task.priority}</span> 374 + </div> 375 + \${task.description ? \`<p class="task-description">\${task.description}</p>\` : ''} 376 + <div class="task-meta"> 377 + <span>Created: \${formatDate(task.createdAt)}</span> 378 + \${task.deadline ? \`<span>Due: \${formatDate(task.deadline)}</span>\` : ''} 379 + </div> 380 + </div> 381 + \`; 382 + } 383 + 384 + titleInput.addEventListener('blur', validateTitle); 385 + titleInput.addEventListener('input', function() { 386 + if (titleError.classList.contains('show')) { 387 + validateTitle(); 388 + } 389 + }); 390 + 391 + form.addEventListener('submit', function(e) { 392 + e.preventDefault(); 393 + 394 + if (!validateTitle()) { 395 + return; 396 + } 397 + 398 + const formData = new FormData(form); 399 + const task = { 400 + id: Date.now().toString(), 401 + title: formData.get('title').trim(), 402 + description: formData.get('description').trim(), 403 + priority: formData.get('priority'), 404 + deadline: formData.get('deadline') || null, 405 + createdAt: new Date().toISOString() 406 + }; 407 + 408 + // Add task to list 409 + const taskElement = createTaskElement(task); 410 + if (taskList.children.length === 0 || taskList.textContent.includes('No tasks yet')) { 411 + taskList.innerHTML = taskElement; 412 + } else { 413 + taskList.insertAdjacentHTML('afterbegin', taskElement); 414 + } 415 + 416 + // Update task count 417 + const currentCount = parseInt(taskCountEl.textContent); 418 + taskCountEl.textContent = currentCount + 1; 419 + 420 + // Reset form 421 + form.reset(); 422 + document.getElementById('priority').value = 'medium'; 423 + }); 424 + })(); 425 + </script> 426 + </body> 427 + </html>`; 428 + } 429 + 430 + private validateTaskForm(formData: TaskFormData): void { 431 + if (!formData.title || formData.title.trim().length === 0) { 432 + throw new Error('Title is required'); 433 + } 434 + } 435 + 436 + private generateId(): string { 437 + return Date.now().toString(36) + Math.random().toString(36).substr(2); 438 + } 439 + 440 + private renderTaskList(): string { 441 + if (this.tasks.length === 0) { 442 + return '<p style="color: var(--gray-500); text-align: center; padding: 2rem;">No tasks yet. Create your first task!</p>'; 443 + } 444 + 445 + return this.tasks 446 + .slice(-5) // Show last 5 tasks 447 + .reverse() 448 + .map(task => ` 449 + <div class="task-item"> 450 + <div class="task-header"> 451 + <h3 class="task-title">${this.escapeHtml(task.title)}</h3> 452 + <span class="priority-badge priority-${task.priority}">${task.priority}</span> 453 + </div> 454 + ${task.description ? `<p class="task-description">${this.escapeHtml(task.description)}</p>` : ''} 455 + <div class="task-meta"> 456 + <span>Created: ${this.formatDate(task.createdAt)}</span> 457 + ${task.deadline ? `<span>Due: ${this.formatDate(task.deadline)}</span>` : ''} 458 + </div> 459 + </div> 460 + `).join(''); 461 + } 462 + 463 + private escapeHtml(text: string): string { 464 + const div = { innerHTML: '' } as any; 465 + div.textContent = text; 466 + return div.innerHTML || text.replace(/[&<>"']/g, (match: string) => { 467 + const escapeMap: Record<string, string> = { 468 + '&': '&amp;', 469 + '<': '&lt;', 470 + '>': '&gt;', 471 + '"': '&quot;', 472 + "'": '&#39;' 473 + }; 474 + return escapeMap[match]; 475 + }); 476 + } 477 + 478 + private formatDate(dateString: string): string { 479 + return new Date(dateString).toLocaleDateString(); 480 + } 481 + } 482 + 483 + export function createDashboard(): DashboardPage { 484 + return new DashboardPage(); 485 + } 486 + 487 + /** @internal Phoenix VCS traceability — do not remove. */ 488 + export const _phoenix = { 489 + iu_id: 'ec4737a7671a24d2c859604470556a65e34e7a700615fa11f18bf5e3d4e5ea88', 490 + name: 'Dashboard Page', 491 + risk_tier: 'high', 492 + canon_ids: [7 as const], 493 + } as const;
+11
examples/taskflow/src/generated/web-dashboard/index.ts
··· 1 + /** 2 + * Web Dashboard 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Barrel export for all Web Dashboard modules. 6 + */ 7 + 8 + export * as analyticsPanel from './analytics-panel.js'; 9 + export * as dashboardPage from './dashboard-page.js'; 10 + export * as styles from './styles.js'; 11 + export * as taskListDisplay from './task-list-display.js';
+125
examples/taskflow/src/generated/web-dashboard/server.ts
··· 1 + /** 2 + * Web Dashboard — HTTP Server 3 + * 4 + * Serves the TaskFlow dashboard as a single-page web app. 5 + * The DashboardPage module generates the complete HTML. 6 + */ 7 + 8 + import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 9 + 10 + import { createDashboard } from './dashboard-page.js'; 11 + import { generateCSSString } from './styles.js'; 12 + import * as analyticsPanel from './analytics-panel.js'; 13 + import * as taskListDisplay from './task-list-display.js'; 14 + 15 + // ─── Metrics ───────────────────────────────────────────────────────────────── 16 + 17 + const _svcMetrics = { 18 + requests_total: 0, 19 + requests_by_path: {} as Record<string, number>, 20 + errors_total: 0, 21 + uptime_start: Date.now(), 22 + }; 23 + 24 + // ─── Module Registry ───────────────────────────────────────────────────────── 25 + 26 + const _svcModules = { 27 + 'analytics-panel': analyticsPanel, 28 + 'dashboard-page': { createDashboard }, 29 + 'styles': { generateCSSString }, 30 + 'task-list-display': taskListDisplay, 31 + }; 32 + 33 + // ─── Router ────────────────────────────────────────────────────────────────── 34 + 35 + type Handler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>; 36 + 37 + const routes: Record<string, Handler> = { 38 + '/': (_req, res) => { 39 + const dashboard = createDashboard(); 40 + const html = dashboard.renderHTML(); 41 + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); 42 + res.end(html); 43 + }, 44 + 45 + '/health': (_req, res) => { 46 + res.writeHead(200, { 'Content-Type': 'application/json' }); 47 + res.end(JSON.stringify({ 48 + status: 'ok', 49 + service: 'Web Dashboard', 50 + uptime: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 51 + modules: Object.keys(_svcModules), 52 + })); 53 + }, 54 + 55 + '/metrics': (_req, res) => { 56 + res.writeHead(200, { 'Content-Type': 'application/json' }); 57 + res.end(JSON.stringify({ 58 + ..._svcMetrics, 59 + uptime_seconds: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 60 + }, null, 2)); 61 + }, 62 + 63 + '/modules': (_req, res) => { 64 + const info = Object.entries(_svcModules).map(([name, mod]) => ({ 65 + name, 66 + exports: Object.keys(mod), 67 + })); 68 + res.writeHead(200, { 'Content-Type': 'application/json' }); 69 + res.end(JSON.stringify(info, null, 2)); 70 + }, 71 + }; 72 + 73 + // ─── Server ────────────────────────────────────────────────────────────────── 74 + 75 + function handleRequest(req: IncomingMessage, res: ServerResponse): void { 76 + const url = req.url ?? '/'; 77 + const path = url.split('?')[0]; 78 + 79 + _svcMetrics.requests_total++; 80 + _svcMetrics.requests_by_path[path] = (_svcMetrics.requests_by_path[path] ?? 0) + 1; 81 + 82 + const handler = routes[path]; 83 + if (handler) { 84 + try { 85 + handler(req, res); 86 + } catch (err) { 87 + _svcMetrics.errors_total++; 88 + res.writeHead(500, { 'Content-Type': 'application/json' }); 89 + res.end(JSON.stringify({ error: String(err) })); 90 + } 91 + } else { 92 + res.writeHead(404, { 'Content-Type': 'application/json' }); 93 + res.end(JSON.stringify({ 94 + error: 'Not Found', 95 + path, 96 + available: Object.keys(routes), 97 + })); 98 + } 99 + } 100 + 101 + export function startServer(port?: number): { server: ReturnType<typeof createServer>; port: number; ready: Promise<void> } { 102 + const requestedPort = port ?? parseInt(process.env.WEB_DASHBOARD_PORT ?? process.env.PORT ?? '3002', 10); 103 + const server = createServer(handleRequest); 104 + let actualPort = requestedPort; 105 + 106 + const ready = new Promise<void>(resolve => { 107 + server.listen(requestedPort, () => { 108 + const addr = server.address(); 109 + if (addr && typeof addr === 'object') actualPort = addr.port; 110 + result.port = actualPort; 111 + console.log(`TaskFlow Dashboard → http://localhost:${actualPort}`); 112 + resolve(); 113 + }); 114 + }); 115 + 116 + const result = { server, port: actualPort, ready }; 117 + return result; 118 + } 119 + 120 + // Start when run directly 121 + const isMain = process.argv[1]?.endsWith('/web-dashboard/server.js') || 122 + process.argv[1]?.endsWith('/web-dashboard/server.ts'); 123 + if (isMain) { 124 + startServer(); 125 + }
+246
examples/taskflow/src/generated/web-dashboard/styles.ts
··· 1 + export interface StyleConfig { 2 + breakpoints: { 3 + mobile: string; 4 + desktop: string; 5 + }; 6 + colors: { 7 + primary: string; 8 + secondary: string; 9 + background: string; 10 + surface: string; 11 + text: string; 12 + textSecondary: string; 13 + border: string; 14 + shadow: string; 15 + }; 16 + typography: { 17 + fontFamily: string; 18 + sizes: { 19 + h1: string; 20 + body: string; 21 + }; 22 + }; 23 + spacing: { 24 + xs: string; 25 + sm: string; 26 + md: string; 27 + lg: string; 28 + xl: string; 29 + }; 30 + borderRadius: { 31 + card: string; 32 + button: string; 33 + }; 34 + } 35 + 36 + export interface ComponentStyles { 37 + layout: string; 38 + card: string; 39 + button: string; 40 + typography: string; 41 + } 42 + 43 + const defaultConfig: StyleConfig = { 44 + breakpoints: { 45 + mobile: '768px', 46 + desktop: '769px' 47 + }, 48 + colors: { 49 + primary: '#007bff', 50 + secondary: '#6c757d', 51 + background: '#ffffff', 52 + surface: '#f8f9fa', 53 + text: '#212529', 54 + textSecondary: '#6c757d', 55 + border: '#dee2e6', 56 + shadow: 'rgba(0, 0, 0, 0.1)' 57 + }, 58 + typography: { 59 + fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', 60 + sizes: { 61 + h1: '1.5rem', 62 + body: '0.95rem' 63 + } 64 + }, 65 + spacing: { 66 + xs: '0.25rem', 67 + sm: '0.5rem', 68 + md: '1rem', 69 + lg: '1.5rem', 70 + xl: '2rem' 71 + }, 72 + borderRadius: { 73 + card: '8px', 74 + button: '4px' 75 + } 76 + }; 77 + 78 + export function generateStyles(config: Partial<StyleConfig> = {}): ComponentStyles { 79 + const mergedConfig: StyleConfig = { 80 + ...defaultConfig, 81 + ...config, 82 + colors: { ...defaultConfig.colors, ...config.colors }, 83 + typography: { ...defaultConfig.typography, ...config.typography }, 84 + spacing: { ...defaultConfig.spacing, ...config.spacing }, 85 + borderRadius: { ...defaultConfig.borderRadius, ...config.borderRadius } 86 + }; 87 + 88 + const layout = ` 89 + .phoenix-layout { 90 + display: grid; 91 + grid-template-columns: 1fr; 92 + gap: ${mergedConfig.spacing.md}; 93 + padding: ${mergedConfig.spacing.md}; 94 + max-width: 100%; 95 + margin: 0 auto; 96 + } 97 + 98 + @media (min-width: ${mergedConfig.breakpoints.desktop}) { 99 + .phoenix-layout { 100 + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 101 + gap: ${mergedConfig.spacing.lg}; 102 + padding: ${mergedConfig.spacing.xl}; 103 + max-width: 1200px; 104 + } 105 + } 106 + `; 107 + 108 + const card = ` 109 + .phoenix-card { 110 + background: ${mergedConfig.colors.surface}; 111 + border: 1px solid ${mergedConfig.colors.border}; 112 + border-radius: ${mergedConfig.borderRadius.card}; 113 + padding: ${mergedConfig.spacing.lg}; 114 + box-shadow: 0 2px 4px ${mergedConfig.colors.shadow}; 115 + transition: box-shadow 0.2s ease, transform 0.2s ease; 116 + } 117 + 118 + .phoenix-card:hover { 119 + box-shadow: 0 4px 8px ${mergedConfig.colors.shadow}; 120 + transform: translateY(-1px); 121 + } 122 + `; 123 + 124 + const button = ` 125 + .phoenix-button { 126 + background: ${mergedConfig.colors.primary}; 127 + color: ${mergedConfig.colors.background}; 128 + border: none; 129 + border-radius: ${mergedConfig.borderRadius.button}; 130 + padding: ${mergedConfig.spacing.sm} ${mergedConfig.spacing.md}; 131 + font-family: ${mergedConfig.typography.fontFamily}; 132 + font-size: ${mergedConfig.typography.sizes.body}; 133 + cursor: pointer; 134 + transition: background-color 0.2s ease, transform 0.1s ease; 135 + display: inline-block; 136 + text-decoration: none; 137 + text-align: center; 138 + } 139 + 140 + .phoenix-button:hover { 141 + background: ${adjustColor(mergedConfig.colors.primary, -10)}; 142 + transform: translateY(-1px); 143 + } 144 + 145 + .phoenix-button:active { 146 + transform: translateY(0); 147 + } 148 + 149 + .phoenix-button--secondary { 150 + background: ${mergedConfig.colors.secondary}; 151 + } 152 + 153 + .phoenix-button--secondary:hover { 154 + background: ${adjustColor(mergedConfig.colors.secondary, -10)}; 155 + } 156 + `; 157 + 158 + const typography = ` 159 + .phoenix-typography { 160 + font-family: ${mergedConfig.typography.fontFamily}; 161 + color: ${mergedConfig.colors.text}; 162 + line-height: 1.5; 163 + } 164 + 165 + .phoenix-typography h1 { 166 + font-size: ${mergedConfig.typography.sizes.h1}; 167 + font-weight: 600; 168 + margin: 0 0 ${mergedConfig.spacing.md} 0; 169 + color: ${mergedConfig.colors.text}; 170 + } 171 + 172 + .phoenix-typography p, 173 + .phoenix-typography div, 174 + .phoenix-typography span { 175 + font-size: ${mergedConfig.typography.sizes.body}; 176 + margin: 0 0 ${mergedConfig.spacing.sm} 0; 177 + } 178 + 179 + .phoenix-typography--secondary { 180 + color: ${mergedConfig.colors.textSecondary}; 181 + } 182 + `; 183 + 184 + return { 185 + layout, 186 + card, 187 + button, 188 + typography 189 + }; 190 + } 191 + 192 + export function injectStyles(styles: ComponentStyles): void { 193 + const styleElement = createStyleElement(); 194 + const combinedStyles = Object.values(styles).join('\n'); 195 + styleElement.textContent = combinedStyles; 196 + } 197 + 198 + export function createStyleElement(): HTMLStyleElement { 199 + if (typeof document === 'undefined') { 200 + throw new Error('createStyleElement can only be used in browser environment'); 201 + } 202 + 203 + const existingStyle = document.getElementById('phoenix-styles'); 204 + if (existingStyle) { 205 + return existingStyle as HTMLStyleElement; 206 + } 207 + 208 + const style = document.createElement('style'); 209 + style.id = 'phoenix-styles'; 210 + style.type = 'text/css'; 211 + document.head.appendChild(style); 212 + return style; 213 + } 214 + 215 + export function generateCSSString(config?: Partial<StyleConfig>): string { 216 + const styles = generateStyles(config); 217 + return Object.values(styles).join('\n'); 218 + } 219 + 220 + function adjustColor(color: string, percent: number): string { 221 + if (!color.startsWith('#')) { 222 + return color; 223 + } 224 + 225 + const num = parseInt(color.slice(1), 16); 226 + const amt = Math.round(2.55 * percent); 227 + const R = (num >> 16) + amt; 228 + const G = (num >> 8 & 0x00FF) + amt; 229 + const B = (num & 0x0000FF) + amt; 230 + 231 + return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + 232 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + 233 + (B < 255 ? B < 1 ? 0 : B : 255)) 234 + .toString(16) 235 + .slice(1); 236 + } 237 + 238 + export const defaultStyleConfig = defaultConfig; 239 + 240 + /** @internal Phoenix VCS traceability — do not remove. */ 241 + export const _phoenix = { 242 + iu_id: 'fd1e1e3084a491150550d575098a4a929a5be62ccb0b000a173679da38aed9fa', 243 + name: 'Styles', 244 + risk_tier: 'medium', 245 + canon_ids: [4 as const], 246 + } as const;
+413
examples/taskflow/src/generated/web-dashboard/task-list-display.ts
··· 1 + export interface Task { 2 + id: string; 3 + title: string; 4 + description: string; 5 + priority: 'critical' | 'high' | 'medium' | 'low'; 6 + status: 'open' | 'in_progress' | 'review' | 'done'; 7 + assignee: string; 8 + deadline: Date; 9 + createdAt: Date; 10 + updatedAt: Date; 11 + } 12 + 13 + export interface TaskFilter { 14 + status?: Task['status'][]; 15 + priority?: Task['priority'][]; 16 + assignee?: string[]; 17 + } 18 + 19 + export interface TaskListDisplayOptions { 20 + onStatusChange?: (taskId: string, newStatus: Task['status']) => void; 21 + onTaskClick?: (task: Task) => void; 22 + } 23 + 24 + export class TaskListDisplay { 25 + private tasks: Task[] = []; 26 + private filter: TaskFilter = {}; 27 + private options: TaskListDisplayOptions; 28 + 29 + constructor(options: TaskListDisplayOptions = {}) { 30 + this.options = options; 31 + } 32 + 33 + setTasks(tasks: Task[]): void { 34 + this.tasks = tasks; 35 + } 36 + 37 + setFilter(filter: TaskFilter): void { 38 + this.filter = filter; 39 + } 40 + 41 + private getFilteredTasks(): Task[] { 42 + return this.tasks.filter(task => { 43 + if (this.filter.status && !this.filter.status.includes(task.status)) { 44 + return false; 45 + } 46 + if (this.filter.priority && !this.filter.priority.includes(task.priority)) { 47 + return false; 48 + } 49 + if (this.filter.assignee && !this.filter.assignee.includes(task.assignee)) { 50 + return false; 51 + } 52 + return true; 53 + }); 54 + } 55 + 56 + private isOverdue(task: Task): boolean { 57 + return new Date() > task.deadline && task.status !== 'done'; 58 + } 59 + 60 + private getPriorityColor(priority: Task['priority']): string { 61 + switch (priority) { 62 + case 'critical': return '#dc2626'; 63 + case 'high': return '#ea580c'; 64 + case 'medium': return '#ca8a04'; 65 + case 'low': return '#16a34a'; 66 + } 67 + } 68 + 69 + private getStatusColor(status: Task['status']): string { 70 + switch (status) { 71 + case 'open': return '#6b7280'; 72 + case 'in_progress': return '#2563eb'; 73 + case 'review': return '#7c3aed'; 74 + case 'done': return '#16a34a'; 75 + } 76 + } 77 + 78 + private getStatusTransitions(currentStatus: Task['status']): Task['status'][] { 79 + switch (currentStatus) { 80 + case 'open': return ['in_progress']; 81 + case 'in_progress': return ['review', 'open']; 82 + case 'review': return ['done', 'in_progress']; 83 + case 'done': return ['open']; 84 + } 85 + } 86 + 87 + private formatDate(date: Date): string { 88 + return date.toLocaleDateString('en-US', { 89 + month: 'short', 90 + day: 'numeric', 91 + year: 'numeric' 92 + }); 93 + } 94 + 95 + private renderTaskCard(task: Task): string { 96 + const isOverdue = this.isOverdue(task); 97 + const priorityColor = this.getPriorityColor(task.priority); 98 + const statusColor = this.getStatusColor(task.status); 99 + const transitions = this.getStatusTransitions(task.status); 100 + 101 + const cardStyle = ` 102 + border: 2px solid ${isOverdue ? '#dc2626' : '#e5e7eb'}; 103 + border-radius: 8px; 104 + padding: 16px; 105 + background: white; 106 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 107 + display: flex; 108 + flex-direction: column; 109 + gap: 12px; 110 + position: relative; 111 + cursor: pointer; 112 + transition: box-shadow 0.2s; 113 + `; 114 + 115 + const titleStyle = ` 116 + font-size: 18px; 117 + font-weight: 600; 118 + color: #111827; 119 + margin: 0; 120 + line-height: 1.4; 121 + `; 122 + 123 + const descriptionStyle = ` 124 + color: #6b7280; 125 + font-size: 14px; 126 + line-height: 1.5; 127 + margin: 0; 128 + `; 129 + 130 + const badgeStyle = ` 131 + display: inline-block; 132 + padding: 4px 8px; 133 + border-radius: 4px; 134 + font-size: 12px; 135 + font-weight: 500; 136 + text-transform: uppercase; 137 + color: white; 138 + `; 139 + 140 + const buttonStyle = ` 141 + padding: 6px 12px; 142 + border: 1px solid #d1d5db; 143 + border-radius: 4px; 144 + background: white; 145 + color: #374151; 146 + font-size: 12px; 147 + cursor: pointer; 148 + transition: background-color 0.2s; 149 + `; 150 + 151 + const overdueIndicator = isOverdue ? ` 152 + <div style=" 153 + position: absolute; 154 + top: 8px; 155 + right: 8px; 156 + background: #dc2626; 157 + color: white; 158 + padding: 2px 6px; 159 + border-radius: 4px; 160 + font-size: 10px; 161 + font-weight: 600; 162 + ">OVERDUE</div> 163 + ` : ''; 164 + 165 + const transitionButtons = transitions.map(status => ` 166 + <button 167 + style="${buttonStyle}" 168 + onclick="window.taskListDisplay?.handleStatusChange('${task.id}', '${status}')" 169 + onmouseover="this.style.backgroundColor='#f3f4f6'" 170 + onmouseout="this.style.backgroundColor='white'" 171 + > 172 + ${status.replace('_', ' ').toUpperCase()} 173 + </button> 174 + `).join(''); 175 + 176 + return ` 177 + <div 178 + style="${cardStyle}" 179 + onclick="window.taskListDisplay?.handleTaskClick('${task.id}')" 180 + onmouseover="this.style.boxShadow='0 4px 6px rgba(0, 0, 0, 0.1)'" 181 + onmouseout="this.style.boxShadow='0 1px 3px rgba(0, 0, 0, 0.1)'" 182 + > 183 + ${overdueIndicator} 184 + 185 + <h3 style="${titleStyle}">${this.escapeHtml(task.title)}</h3> 186 + 187 + <p style="${descriptionStyle}">${this.escapeHtml(task.description)}</p> 188 + 189 + <div style="display: flex; gap: 8px; flex-wrap: wrap;"> 190 + <span style="${badgeStyle}; background-color: ${priorityColor};"> 191 + ${task.priority} 192 + </span> 193 + <span style="${badgeStyle}; background-color: ${statusColor};"> 194 + ${task.status.replace('_', ' ')} 195 + </span> 196 + </div> 197 + 198 + <div style="display: flex; justify-content: space-between; align-items: center; font-size: 14px; color: #6b7280;"> 199 + <span>Assigned to: ${this.escapeHtml(task.assignee)}</span> 200 + <span>Due: ${this.formatDate(task.deadline)}</span> 201 + </div> 202 + 203 + <div style="display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px;"> 204 + ${transitionButtons} 205 + </div> 206 + </div> 207 + `; 208 + } 209 + 210 + private escapeHtml(text: string): string { 211 + const div = { innerHTML: '' } as any; 212 + div.textContent = text; 213 + return div.innerHTML || text.replace(/[&<>"']/g, (match: string) => { 214 + const escapeMap: Record<string, string> = { 215 + '&': '&amp;', 216 + '<': '&lt;', 217 + '>': '&gt;', 218 + '"': '&quot;', 219 + "'": '&#39;' 220 + }; 221 + return escapeMap[match]; 222 + }); 223 + } 224 + 225 + render(): string { 226 + const filteredTasks = this.getFilteredTasks(); 227 + 228 + const gridStyle = ` 229 + display: grid; 230 + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); 231 + gap: 20px; 232 + padding: 20px; 233 + max-width: 1200px; 234 + margin: 0 auto; 235 + `; 236 + 237 + const emptyStateStyle = ` 238 + text-align: center; 239 + padding: 40px; 240 + color: #6b7280; 241 + font-size: 16px; 242 + `; 243 + 244 + if (filteredTasks.length === 0) { 245 + return ` 246 + <div style="${emptyStateStyle}"> 247 + No tasks match the current filters. 248 + </div> 249 + `; 250 + } 251 + 252 + const taskCards = filteredTasks.map(task => this.renderTaskCard(task)).join(''); 253 + 254 + return ` 255 + <div style="${gridStyle}"> 256 + ${taskCards} 257 + </div> 258 + <script> 259 + window.taskListDisplay = { 260 + handleStatusChange: (taskId, newStatus) => { 261 + if (window.taskListDisplayInstance?.options.onStatusChange) { 262 + window.taskListDisplayInstance.options.onStatusChange(taskId, newStatus); 263 + } 264 + }, 265 + handleTaskClick: (taskId) => { 266 + if (window.taskListDisplayInstance?.options.onTaskClick) { 267 + const task = window.taskListDisplayInstance.tasks.find(t => t.id === taskId); 268 + if (task) { 269 + window.taskListDisplayInstance.options.onTaskClick(task); 270 + } 271 + } 272 + } 273 + }; 274 + </script> 275 + `; 276 + } 277 + 278 + attachToGlobal(): void { 279 + (globalThis as any).taskListDisplayInstance = this; 280 + } 281 + } 282 + 283 + export function createTaskListDisplay(options?: TaskListDisplayOptions): TaskListDisplay { 284 + const display = new TaskListDisplay(options); 285 + display.attachToGlobal(); 286 + return display; 287 + } 288 + 289 + export function renderTaskFilter( 290 + availableStatuses: Task['status'][], 291 + availablePriorities: Task['priority'][], 292 + availableAssignees: string[], 293 + currentFilter: TaskFilter, 294 + onFilterChange: (filter: TaskFilter) => void 295 + ): string { 296 + const filterStyle = ` 297 + display: flex; 298 + gap: 16px; 299 + padding: 16px; 300 + background: #f9fafb; 301 + border-radius: 8px; 302 + margin-bottom: 20px; 303 + flex-wrap: wrap; 304 + `; 305 + 306 + const selectStyle = ` 307 + padding: 8px 12px; 308 + border: 1px solid #d1d5db; 309 + border-radius: 4px; 310 + background: white; 311 + font-size: 14px; 312 + `; 313 + 314 + const labelStyle = ` 315 + font-weight: 500; 316 + color: #374151; 317 + margin-bottom: 4px; 318 + display: block; 319 + `; 320 + 321 + return ` 322 + <div style="${filterStyle}"> 323 + <div> 324 + <label style="${labelStyle}">Status:</label> 325 + <select 326 + style="${selectStyle}" 327 + multiple 328 + onchange="window.updateFilter('status', Array.from(this.selectedOptions).map(o => o.value))" 329 + > 330 + ${availableStatuses.map(status => ` 331 + <option value="${status}" ${currentFilter.status?.includes(status) ? 'selected' : ''}> 332 + ${status.replace('_', ' ').toUpperCase()} 333 + </option> 334 + `).join('')} 335 + </select> 336 + </div> 337 + 338 + <div> 339 + <label style="${labelStyle}">Priority:</label> 340 + <select 341 + style="${selectStyle}" 342 + multiple 343 + onchange="window.updateFilter('priority', Array.from(this.selectedOptions).map(o => o.value))" 344 + > 345 + ${availablePriorities.map(priority => ` 346 + <option value="${priority}" ${currentFilter.priority?.includes(priority) ? 'selected' : ''}> 347 + ${priority.toUpperCase()} 348 + </option> 349 + `).join('')} 350 + </select> 351 + </div> 352 + 353 + <div> 354 + <label style="${labelStyle}">Assignee:</label> 355 + <select 356 + style="${selectStyle}" 357 + multiple 358 + onchange="window.updateFilter('assignee', Array.from(this.selectedOptions).map(o => o.value))" 359 + > 360 + ${availableAssignees.map(assignee => ` 361 + <option value="${assignee}" ${currentFilter.assignee?.includes(assignee) ? 'selected' : ''}> 362 + ${assignee} 363 + </option> 364 + `).join('')} 365 + </select> 366 + </div> 367 + 368 + <button 369 + style="${selectStyle}; cursor: pointer; background: #ef4444; color: white; border-color: #dc2626;" 370 + onclick="window.clearFilters()" 371 + > 372 + Clear Filters 373 + </button> 374 + </div> 375 + 376 + <script> 377 + window.updateFilter = (type, values) => { 378 + const newFilter = { ...window.currentTaskFilter }; 379 + if (values.length === 0) { 380 + delete newFilter[type]; 381 + } else { 382 + newFilter[type] = values; 383 + } 384 + window.currentTaskFilter = newFilter; 385 + if (window.onTaskFilterChange) { 386 + window.onTaskFilterChange(newFilter); 387 + } 388 + }; 389 + 390 + window.clearFilters = () => { 391 + window.currentTaskFilter = {}; 392 + if (window.onTaskFilterChange) { 393 + window.onTaskFilterChange({}); 394 + } 395 + // Reset all selects 396 + document.querySelectorAll('select').forEach(select => { 397 + select.selectedIndex = -1; 398 + }); 399 + }; 400 + 401 + window.currentTaskFilter = ${JSON.stringify(currentFilter)}; 402 + window.onTaskFilterChange = ${onFilterChange.toString()}; 403 + </script> 404 + `; 405 + } 406 + 407 + /** @internal Phoenix VCS traceability — do not remove. */ 408 + export const _phoenix = { 409 + iu_id: 'a6550cdc3ef254c13571c1134a3f1ad230c942e0325c50f89ae97502a302fd01', 410 + name: 'Task List Display', 411 + risk_tier: 'medium', 412 + canon_ids: [7 as const], 413 + } as const;