@pimzino/claude-code-spec-workflow
Version:
Automated workflows for Claude Code. Includes spec-driven development (Requirements → Design → Tasks → Implementation) with intelligent orchestration, optional steering documents and streamlined bug fix workflow (Report → Analyze → Fix → Verify). We have
1,036 lines (971 loc) • 53.7 kB
HTML
<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Claude Spec Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
// Suppress Tailwind CDN production warning for development dashboard
if (typeof tailwind !== 'undefined') {
tailwind.config = { devtools: { enabled: false } };
}
</script>
<script src="https://unpkg.com/petite-vue@0.4.1/dist/petite-vue.iife.js"></script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
/>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
[v-cloak] {
display: none;
}
.tab-active {
border-bottom-color: #6366f1;
color: #6366f1;
}
/* EARS keyword syntax highlighting */
.ears-keyword {
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 0.875em;
font-weight: 500;
color: rgb(245 158 11);
}
.dark .ears-keyword {
color: rgb(251 191 36);
}
/* Remove focus ring from markdown preview buttons */
.markdown-preview-btn:focus {
outline: none;
}
/* GitHub-style Markdown content */
.markdown-content {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
color: #24292f;
}
.dark .markdown-content {
color: #c9d1d9;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
color: #24292f;
}
.dark .markdown-content h1,
.dark .markdown-content h2,
.dark .markdown-content h3,
.dark .markdown-content h4,
.dark .markdown-content h5,
.dark .markdown-content h6 {
color: #c9d1d9;
}
.markdown-content h1 {
font-size: 2em;
padding-bottom: 0.3em;
border-bottom: 1px solid #d0d7de;
}
.dark .markdown-content h1 {
border-bottom-color: #21262d;
}
.markdown-content h2 {
font-size: 1.5em;
padding-bottom: 0.3em;
border-bottom: 1px solid #d0d7de;
}
.dark .markdown-content h2 {
border-bottom-color: #21262d;
}
.markdown-content h3 { font-size: 1.25em; }
.markdown-content h4 { font-size: 1em; }
.markdown-content h5 { font-size: 0.875em; }
.markdown-content h6 { font-size: 0.85em; color: #57606a; }
.dark .markdown-content h6 { color: #8b949e; }
.markdown-content p {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-content code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
white-space: break-spaces;
background-color: rgba(175, 184, 193, 0.2);
border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
}
.dark .markdown-content code {
background-color: rgba(110, 118, 129, 0.4);
}
.markdown-content pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 6px;
margin-top: 0;
margin-bottom: 16px;
}
.dark .markdown-content pre {
background-color: #161b22;
}
.markdown-content pre code {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
font-size: 100%;
}
.markdown-content ul,
.markdown-content ol {
margin-top: 0;
margin-bottom: 16px;
padding-left: 2em;
}
.markdown-content ul ul,
.markdown-content ul ol,
.markdown-content ol ol,
.markdown-content ol ul {
margin-bottom: 0;
}
.markdown-content li {
margin-top: 0.25em;
}
.markdown-content li + li {
margin-top: 0.25em;
}
.markdown-content blockquote {
padding: 0 1em;
color: #57606a;
border-left: 0.25em solid #d0d7de;
margin: 0 0 16px 0;
}
.dark .markdown-content blockquote {
color: #8b949e;
border-left-color: #3b434b;
}
.markdown-content blockquote > :first-child {
margin-top: 0;
}
.markdown-content blockquote > :last-child {
margin-bottom: 0;
}
.markdown-content a {
color: #0969da;
text-decoration: none;
font-weight: 500;
}
.dark .markdown-content a {
color: #58a6ff;
}
.markdown-content a:hover {
text-decoration: underline;
}
.markdown-content table {
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
border-spacing: 0;
border-collapse: collapse;
margin-top: 0;
margin-bottom: 16px;
}
.markdown-content table tr {
background-color: #ffffff;
border-top: 1px solid #d1d9e0;
}
.dark .markdown-content table tr {
background-color: #0d1117;
border-top-color: #30363d;
}
.markdown-content table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.dark .markdown-content table tr:nth-child(2n) {
background-color: #161b22;
}
.markdown-content table th,
.markdown-content table td {
padding: 6px 13px;
border: 1px solid #d0d7de;
}
.dark .markdown-content table th,
.dark .markdown-content table td {
border-color: #30363d;
}
.markdown-content table th {
font-weight: 600;
}
.markdown-content hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #d0d7de;
border: 0;
}
.dark .markdown-content hr {
background-color: #21262d;
}
.markdown-content img {
max-width: 100%;
box-sizing: content-box;
background-color: #ffffff;
}
.dark .markdown-content img {
background-color: #0d1117;
}
.markdown-content > *:first-child {
margin-top: 0 !important;
}
.markdown-content > *:last-child {
margin-bottom: 0 !important;
}
</style>
<script>
tailwind.config = {
darkMode: 'class',
};
</script>
</head>
<body class="h-full bg-gray-50 dark:bg-gray-900 transition-colors">
<div id="app" v-cloak class="h-full flex flex-col" @vue:mounted="init()">
<!-- Header -->
<header
class="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700"
>
<div class="max-w-full px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center py-3">
<div class="flex items-center">
<img src="/public/claude-icon.svg" alt="Claude" class="w-6 h-6 mr-2" />
<h1 class="text-xl font-bold text-gray-900 dark:text-white">
{{ username }}'s Dashboard
</h1>
</div>
<div class="flex items-center gap-3">
<span class="text-sm text-gray-600 dark:text-gray-400">
{{ projects.length }} projects
</span>
<!-- Theme toggle -->
<button
@click="cycleTheme"
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
title="Toggle theme"
>
<svg
v-if="theme === 'light'"
class="w-5 h-5 text-gray-600 dark:text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
></path>
</svg>
<svg
v-else-if="theme === 'dark'"
class="w-5 h-5 text-gray-600 dark:text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
></path>
</svg>
<svg
v-else
class="w-5 h-5 text-gray-600 dark:text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
></path>
</svg>
</button>
<i
class="fas fa-circle text-xs"
:class="connected ? 'text-green-500' : 'text-red-500'"
:title="connected ? 'Connected' : 'Disconnected'"
></i>
</div>
</div>
</div>
</header>
<!-- Tab Navigation -->
<div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div class="flex px-4 sm:px-6 lg:px-8">
<!-- Active Tasks Tab -->
<button
@click="switchTab('active')"
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap flex items-center gap-2 mr-2"
:class="activeTab === 'active' ? 'tab-active border-indigo-600' : 'text-gray-600 dark:text-gray-400 border-transparent hover:text-gray-900 dark:hover:text-white hover:border-gray-300 dark:hover:border-gray-600'"
>
<i class="fas fa-bolt"></i>
Active Sessions
<span
v-if="activeSessionCount > 0"
class="bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 text-xs px-2 py-0.5 rounded-full"
>{{ activeSessionCount }}</span
>
</button>
<!-- Divider -->
<div class="border-l border-gray-300 dark:border-gray-600 my-2 mx-2"></div>
<!-- Project Tabs -->
<div class="flex overflow-x-auto">
<button
v-for="project in projects"
:key="project.path"
@click="selectedProject = project; activeTab = 'projects'"
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap flex items-center gap-2"
:class="activeTab === 'projects' && selectedProject?.path === project.path ? 'tab-active border-indigo-600' : 'text-gray-600 dark:text-gray-400 border-transparent hover:text-gray-900 dark:hover:text-white hover:border-gray-300 dark:hover:border-gray-600'"
>
<span
v-if="project.hasActiveSession"
class="w-2 h-2 bg-green-500 rounded-full animate-pulse"
title="Active Claude session"
></span>
{{ project.name }}
<span
v-if="(getOpenSpecsCount(project) + getOpenBugsCount(project)) > 0"
class="text-xs text-gray-500 dark:text-gray-500"
>({{ getOpenSpecsCount(project) }}/{{ getOpenBugsCount(project) }})</span
>
</button>
</div>
</div>
</div>
<!-- Main content -->
<main class="flex-1 overflow-y-auto">
<!-- Active Tasks View -->
<div v-if="activeTab === 'active'" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div v-if="activeTasks.length === 0" class="text-center py-12">
<i class="fas fa-bolt text-4xl text-gray-400 dark:text-gray-600 mb-4"></i>
<p class="text-gray-500 dark:text-gray-400">
No tasks currently being worked on in active Claude sessions
</p>
<p class="text-sm text-gray-400 dark:text-gray-500 mt-2">
Tasks marked as "In Progress" in active sessions will appear here
</p>
</div>
<div v-else class="space-y-6">
<div
v-for="activeTask in activeTasks"
:key="`${activeTask.projectPath}-${activeTask.specName}-${activeTask.task.id}`"
class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 hover:shadow-lg transition-shadow cursor-pointer"
:class="{'ring-2 ring-yellow-500 dark:ring-yellow-400': activeTask.isCurrentlyActive}"
@click="selectProjectFromTask(activeTask.projectPath, activeTask.specName)"
>
<div class="flex items-start justify-between">
<div class="flex-1">
<!-- Project/Spec Names - Prominent -->
<div class="mb-4">
<div class="flex items-center gap-2 mb-2">
<span
v-if="activeTask.isCurrentlyActive"
class="w-3 h-3 bg-yellow-500 rounded-full animate-pulse"
title="Currently active"
></span>
<h2 class="text-xl font-bold text-gray-900 dark:text-white">
<a
@click="selectProjectFromTask(activeTask.projectPath, activeTask.specName)"
class="hover:text-indigo-600 dark:hover:text-indigo-400 cursor-pointer"
>
{{ activeTask.projectName }}: {{ activeTask.specDisplayName }}
</a>
</h2>
</div>
<!-- Git info pills -->
<div class="flex items-center gap-2 mt-2">
<span
v-if="activeTask.gitBranch"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
>
<i class="fas fa-code-branch mr-1"></i>
{{ activeTask.gitBranch }}
</span>
<span
v-if="activeTask.gitCommit"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
>
<i class="fas fa-code-commit mr-1"></i>
{{ activeTask.gitCommit }}
</span>
</div>
</div>
<!-- Current Task -->
<div class="mb-4">
<div class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1">
Current Task
</div>
<div class="flex items-center flex-wrap gap-2">
<h4 class="text-lg font-medium text-gray-900 dark:text-white">
Task {{ activeTask.task.id }}: {{ activeTask.task.description }}
</h4>
<button
@click.stop="copyTaskCommand(activeTask.specName, activeTask.task.id, $event)"
class="text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300 flex items-center gap-1"
:title="`Copy command: /spec-execute ${activeTask.specName} ${activeTask.task.id}`"
>
<i class="fas fa-copy"></i>
<span>/spec-execute</span>
</button>
</div>
</div>
<!-- Task Progress -->
<div class="mb-3">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
Task {{ getTaskNumber(activeTask) }} of {{ getSpecTaskCount(activeTask) }}
</span>
<span class="text-sm text-gray-500 dark:text-gray-400">
{{ Math.round(getSpecProgress(activeTask)) }}% complete
</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
<div
class="bg-indigo-600 dark:bg-indigo-500 h-2.5 rounded-full transition-all duration-300"
:style="`width: ${getSpecProgress(activeTask)}%`"
></div>
</div>
</div>
<!-- Next Task -->
<div v-if="getNextTask(activeTask)" class="mb-3">
<div class="flex items-center flex-wrap gap-2">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300"
>Next Up:</span
>
<span class="text-sm text-gray-600 dark:text-gray-400">
Task {{ getNextTask(activeTask).id }}: {{ getNextTask(activeTask).description
}}
</span>
<button
@click.stop="copyTaskCommand(activeTask.specName, getNextTask(activeTask).id, $event)"
class="text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300 flex items-center gap-1"
:title="`Copy command: /spec-execute ${activeTask.specName} ${getNextTask(activeTask).id}`"
>
<i class="fas fa-copy"></i>
<span>/spec-execute</span>
</button>
</div>
</div>
<!-- Task Details - Flattened on one line -->
<div class="flex flex-wrap gap-4 text-sm">
<span
v-if="activeTask.task.requirements && activeTask.task.requirements.length > 0"
class="text-gray-600 dark:text-gray-400"
>
<i class="fas fa-clipboard-check mr-1"></i>
Req: {{ activeTask.task.requirements.join(', ') }}
</span>
<span v-if="activeTask.task.leverage" class="text-gray-600 dark:text-gray-400">
<i class="fas fa-screwdriver mr-1"></i>
Leverage: {{ activeTask.task.leverage }}
</span>
</div>
</div>
<div class="ml-4">
<span
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
>
<i class="fas fa-bolt mr-1"></i> Active
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Projects View -->
<div
v-if="activeTab === 'projects' && selectedProject"
class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"
>
<!-- Project Info -->
<div class="mb-4 flex items-center justify-between">
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ selectedProject.name }}
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">{{ selectedProject.path }}</p>
<!-- Git info pills -->
<div class="flex items-center gap-2 mt-2">
<span
v-if="selectedProject.gitBranch"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
>
<i class="fas fa-code-branch mr-1"></i>
{{ selectedProject.gitBranch }}
</span>
<span
v-if="selectedProject.gitCommit"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
>
<i class="fas fa-code-commit mr-1"></i>
{{ selectedProject.gitCommit }}
</span>
</div>
</div>
<div class="flex items-center gap-4 text-sm">
<div class="flex items-center gap-1.5"
:title="selectedProject.steering && selectedProject.steering.exists ? (selectedProject.steering.hasProduct && selectedProject.steering.hasTech && selectedProject.steering.hasStructure ? 'Steering documents complete' : 'Steering documents incomplete') : 'No steering documents'">
<i class="fas fa-compass"
:class="selectedProject.steering && selectedProject.steering.hasProduct && selectedProject.steering.hasTech && selectedProject.steering.hasStructure ? 'text-green-500' : 'text-gray-400 dark:text-gray-500'"></i>
<span class="hidden md:inline text-gray-600 dark:text-gray-400">Steering:</span>
<i class="fas fa-sm"
:class="selectedProject.steering && selectedProject.steering.hasProduct && selectedProject.steering.hasTech && selectedProject.steering.hasStructure ? 'fa-check text-green-600 dark:text-green-400' : 'fa-times text-red-500 dark:text-red-400'"></i>
</div>
<div class="flex items-center gap-1.5">
<i class="fas fa-file-alt text-gray-400 dark:text-gray-500"></i>
<span class="hidden md:inline text-gray-600 dark:text-gray-400">Specs:</span>
<span class="font-semibold text-gray-900 dark:text-white"
>{{ selectedProject.specs?.length || 0 }}</span
>
</div>
<div class="flex items-center gap-1.5">
<i class="fas fa-spinner text-indigo-400 dark:text-indigo-500"></i>
<span class="hidden md:inline text-gray-600 dark:text-gray-400">In Progress:</span>
<span class="font-semibold text-indigo-600 dark:text-indigo-400"
>{{ getSpecsInProgress(selectedProject) }}</span
>
</div>
<div class="flex items-center gap-1.5">
<i class="fas fa-check-circle text-green-400 dark:text-green-500"></i>
<span class="hidden md:inline text-gray-600 dark:text-gray-400">Completed:</span>
<span class="font-semibold text-green-600 dark:text-green-400"
>{{ getSpecsCompleted(selectedProject) }}</span
>
</div>
<div class="flex items-center gap-1.5">
<i class="fas fa-tasks text-gray-400 dark:text-gray-500"></i>
<span class="hidden md:inline text-gray-600 dark:text-gray-400">Tasks:</span>
<span class="font-semibold text-gray-900 dark:text-white"
>{{ getTotalTasks(selectedProject) }}</span
>
</div>
<div v-if="(selectedProject.bugs?.length || 0) > 0" class="flex items-center gap-1.5">
<i class="fas fa-bug text-gray-400 dark:text-gray-500"></i>
<span class="hidden md:inline text-gray-600 dark:text-gray-400">Bugs:</span>
<span class="font-semibold text-gray-900 dark:text-white"
>{{ selectedProject.bugs?.length || 0 }}</span
>
</div>
<div v-if="getBugsInProgress(selectedProject) > 0" class="flex items-center gap-1.5">
<span class="hidden md:inline text-gray-600 dark:text-gray-400"
>Active:</span
>
<span class="font-semibold text-orange-600 dark:text-orange-400"
>{{ getBugsInProgress(selectedProject) }}</span
>
</div>
<div v-if="getBugsResolved(selectedProject) > 0" class="flex items-center gap-1.5">
<span class="hidden md:inline text-gray-600 dark:text-gray-400"
>Resolved:</span
>
<span class="font-semibold text-green-600 dark:text-green-400"
>{{ getBugsResolved(selectedProject) }}</span
>
</div>
</div>
</div>
<!-- Steering Documents Warning (Only shown when incomplete) -->
<div v-if="selectedProject.steering && (!selectedProject.steering.exists || !selectedProject.steering.hasProduct || !selectedProject.steering.hasTech || !selectedProject.steering.hasStructure)"
class="mb-6 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<div class="flex">
<div class="flex-shrink-0">
<i class="fas fa-exclamation-triangle text-yellow-400"></i>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
Steering Documents Incomplete
</h3>
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
<p>
Run <button @click.stop="copyCommand('/spec-steering-setup', $event)" class="inline-flex items-center gap-1 bg-yellow-100 dark:bg-yellow-800 px-1.5 py-0.5 rounded text-xs font-mono hover:bg-yellow-200 dark:hover:bg-yellow-700 transition-colors"><i class="fas fa-copy"></i>/spec-steering-setup</button> to create missing steering documents:
</p>
<ul class="list-disc list-inside mt-1">
<li v-if="!selectedProject.steering.hasProduct">Product vision and goals (product.md)</li>
<li v-if="!selectedProject.steering.hasTech">Technology stack and architecture (tech.md)</li>
<li v-if="!selectedProject.steering.hasStructure">Project structure and patterns (structure.md)</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Specs list -->
<div class="bg-white dark:bg-gray-800 shadow rounded-lg">
<div class="divide-y divide-gray-200 dark:divide-gray-700">
<div
v-for="spec in selectedProject.specs"
:key="spec.name"
class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-all"
:class="spec.status === 'completed' ? 'opacity-75' : ''"
>
<div
class="flex items-center justify-between p-6 cursor-pointer"
@click="selectedSpec = selectedSpec?.name === spec.name ? null : spec"
>
<div class="flex-1">
<h3 class="text-lg font-medium transition-all"
:class="spec.status === 'completed' ? 'text-gray-600 dark:text-gray-400' : 'text-gray-900 dark:text-white'">
{{ spec.displayName }}
</h3>
<div
class="mt-1 flex items-center space-x-4 text-sm"
:class="spec.status === 'completed' ? 'text-gray-400 dark:text-gray-500' : 'text-gray-500 dark:text-gray-400'"
>
<span>
<i class="fas fa-clock mr-1"></i>
{{ formatDate(spec.lastModified) }}
</span>
<span v-if="spec.tasks">
<i class="fas fa-tasks mr-1"></i>
{{ spec.tasks.completed }} / {{ spec.tasks.total }} tasks
</span>
</div>
</div>
<div class="ml-4 flex items-center gap-3">
<span
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
:class="getStatusClass(spec.status)"
>
{{ getStatusLabel(spec.status) }}
</span>
<i
class="fas fa-chevron-down text-gray-400 transition-transform duration-200"
:class="selectedSpec?.name === spec.name ? 'rotate-180' : ''"
></i>
</div>
</div>
<!-- Progress bar (outside clickable area) -->
<div v-if="spec.tasks && spec.tasks.total > 0" class="px-6 pb-3">
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
class="bg-indigo-600 dark:bg-indigo-500 h-2 rounded-full transition-all duration-300"
:style="`width: ${(spec.tasks.completed / spec.tasks.total) * 100}%`"
></div>
</div>
</div>
<!-- Expanded details (outside clickable area) -->
<div
v-if="selectedSpec?.name === spec.name"
class="px-6 pb-6 pt-4 border-t border-gray-200 dark:border-gray-700"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
<div>
<h4 class="font-medium text-gray-900 dark:text-white mb-2">
<i class="fas fa-clipboard-list mr-1"></i> Requirements
<button
v-if="spec.requirements"
@click.stop="viewMarkdown(spec.name, 'requirements')"
class="markdown-preview-btn ml-2 text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300"
title="View source"
>
<i class="fas fa-search"></i> <span class="font-mono text-[10px] opacity-75">.md</span>
</button>
</h4>
<div
v-if="spec.requirements"
class="text-sm text-gray-600 dark:text-gray-400"
>
<div class="flex items-center justify-between mb-2">
<p class="flex items-center">
<i
class="fas mr-1"
:class="spec.requirements.approved ? 'fa-check-circle text-green-500' : 'fa-clock text-yellow-500'"
></i>
{{ spec.requirements.content?.length || 0 }} requirements - {{
spec.requirements.approved ? 'Approved' : 'Pending' }}
</p>
<button
v-if="spec.requirements.content && spec.requirements.content.length > 0"
@click.stop="toggleRequirementsExpanded(spec.name)"
class="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 flex items-center gap-1"
>
<i class="fas" :class="isRequirementsExpanded(spec.name) ? 'fa-eye-slash' : 'fa-eye'"></i>
<span>{{ isRequirementsExpanded(spec.name) ? 'Hide' : 'Show' }} details</span>
</button>
</div>
<!-- Expanded view - full details -->
<div
v-if="spec.requirements.content && spec.requirements.content.length > 0 && isRequirementsExpanded(spec.name)"
class="space-y-3"
>
<!-- Requirements header with clickable links -->
<div class="bg-indigo-50 dark:bg-indigo-900/20 rounded-lg p-3 mb-3">
<h5 class="text-sm font-medium text-indigo-900 dark:text-indigo-200 mb-2">Jump to Requirement:</h5>
<div class="flex flex-wrap gap-2">
<a
v-for="(requirement, index) in spec.requirements.content"
:key="(requirement && requirement.id) || index"
v-if="requirement && requirement.id"
:href="`#${spec.name}-req-${requirement.id}`"
@click.prevent="scrollToRequirement(spec.name, requirement.id)"
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-white dark:bg-gray-800 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-800 transition-colors border border-indigo-200 dark:border-indigo-700"
>
{{ requirement.id }}: {{ requirement.title || 'No title' }}
</a>
</div>
</div>
<div
v-for="(requirement, index) in spec.requirements.content"
:key="(requirement && requirement.id) || index"
:id="`${spec.name}-req-${requirement.id}`"
class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3 scroll-mt-20"
>
<div v-if="requirement && requirement.id">
<h5 class="text-sm font-medium text-gray-900 dark:text-white mb-2">
<strong>{{ requirement.id }}:</strong> {{
requirement.title || 'No title' }}
</h5>
<div
v-if="requirement.userStory"
class="text-xs text-blue-600 dark:text-blue-400 mb-2 italic"
v-html="formatUserStory(requirement.userStory)"
>
</div>
<div
v-if="requirement.acceptanceCriteria && requirement.acceptanceCriteria.length > 0"
>
<p
class="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Acceptance Criteria:
</p>
<div class="space-y-1">
<div
v-for="(criteria, index) in requirement.acceptanceCriteria"
:key="index"
class="flex items-start text-xs text-gray-600 dark:text-gray-400"
>
<span class="mr-2">•</span>
<span v-html="formatAcceptanceCriteria(criteria)"></span>
</div>
</div>
</div>
</div>
<div
v-else-if="typeof requirement === 'string'"
class="text-xs text-gray-600 dark:text-gray-400"
>
{{ requirement }}
</div>
<div v-else class="text-xs text-gray-600 dark:text-gray-400">
<pre>{{ JSON.stringify(requirement, null, 2) }}</pre>
</div>
</div>
</div>
</div>
<p v-else class="text-sm text-gray-400 dark:text-gray-500">Not created</p>
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-white mb-2">
<i class="fas fa-drafting-compass mr-1"></i> Design
<button
v-if="spec.design"
@click.stop="viewMarkdown(spec.name, 'design')"
class="markdown-preview-btn ml-2 text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300"
title="View source"
>
<i class="fas fa-search"></i> <span class="font-mono text-[10px] opacity-75">.md</span>
</button>
</h4>
<div v-if="spec.design" class="text-sm text-gray-600 dark:text-gray-400">
<div class="flex items-center justify-between mb-2">
<p class="flex items-center">
<i
class="fas mr-1"
:class="spec.design.approved ? 'fa-check-circle text-green-500' : 'fa-clock text-yellow-500'"
></i>
{{ spec.design.approved ? 'Approved' : 'Pending' }}
<i
class="fas ml-2"
:class="spec.design.hasCodeReuseAnalysis ? 'fa-check-circle text-green-500' : 'fa-times-circle text-red-500'"
></i>
<span class="ml-1">{{ spec.design.hasCodeReuseAnalysis ? 'Has code reuse analysis' : 'No code reuse analysis' }}</span>
</p>
<button
v-if="spec.design.codeReuseContent && spec.design.codeReuseContent.length > 0"
@click.stop="toggleDesignExpanded(spec.name)"
class="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 flex items-center gap-1"
>
<i class="fas" :class="isDesignExpanded(spec.name) ? 'fa-eye-slash' : 'fa-eye'"></i>
<span>{{ isDesignExpanded(spec.name) ? 'Hide' : 'Show' }} details</span>
</button>
</div>
<div
v-if="spec.design.codeReuseContent && spec.design.codeReuseContent.length > 0 && isDesignExpanded(spec.name)"
class="space-y-2 mt-2"
>
<div
v-for="category in spec.design.codeReuseContent"
:key="category.title"
class="bg-green-50 dark:bg-green-900/20 rounded-lg p-2"
>
<h6 class="text-xs font-medium text-green-700 dark:text-green-300 mb-1">
{{ category.title }}
</h6>
<ul class="text-xs text-green-600 dark:text-green-400 space-y-0.5">
<li
v-for="item in category.items"
:key="item"
class="flex items-start"
>
<span
class="mr-1.5 mt-0.5 w-1 h-1 bg-green-500 rounded-full flex-shrink-0"
></span>
<span>{{ item }}</span>
</li>
</ul>
</div>
</div>
</div>
<p v-else class="text-sm text-gray-400 dark:text-gray-500">Not created</p>
</div>
</div>
<!-- Task list -->
<div v-if="spec.tasks && spec.tasks.taskList.length > 0" class="mt-4">
<div class="mb-2 flex items-center justify-between">
<h4 class="font-medium text-gray-700 dark:text-gray-300">
<i class="fas fa-tasks mr-1"></i> Tasks ({{ spec.tasks.total }} tasks -
<i
class="fas mr-1"
:class="spec.tasks.approved ? 'fa-check-circle text-green-500' : 'fa-clock text-yellow-500'"
></i>
{{ spec.tasks.approved ? 'Approved' : 'Pending' }})
<button
@click.stop="viewMarkdown(spec.name, 'tasks')"
class="markdown-preview-btn ml-2 text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300"
title="View source"
>
<i class="fas fa-search"></i> <span class="font-mono text-[10px] opacity-75">.md</span>
</button>
</h4>
<button
v-if="getCompletedTaskCount(spec) > 0"
@click.stop="toggleCompletedTasks(spec.name)"
class="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 flex items-center gap-1"
>
<i class="fas" :class="areCompletedTasksCollapsed(spec.name) ? 'fa-eye' : 'fa-eye-slash'"></i>
<span>{{ areCompletedTasksCollapsed(spec.name) ? 'Show' : 'Hide' }} {{ getCompletedTaskCount(spec) }} completed</span>
</button>
</div>
<div class="space-y-1">
<div
v-for="task in getVisibleTasks(spec)"
:key="task.id"
class="flex items-start p-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-opacity"
:class="{
'bg-yellow-50 dark:bg-yellow-900/50 border-2 border-yellow-500 dark:border-yellow-400': spec.tasks.inProgress === task.id,
'opacity-60': task.completed
}"
>
<i
class="fas mt-0.5 mr-2"
:class="task.completed ? 'fa-check-square text-green-500' : 'fa-square text-gray-400'"
></i>
<div class="flex-1">
<div class="flex items-center flex-wrap gap-2">
<span class="font-medium text-sm"
:class="task.completed ? 'text-gray-500 dark:text-gray-500 line-through' : 'text-gray-900 dark:text-gray-200'"
>Task {{ task.id }}:</span
>
<span class="text-sm"
:class="task.completed ? 'text-gray-500 dark:text-gray-500 line-through' : 'text-gray-700 dark:text-gray-300'"
>{{ task.description }}</span
>
<button
v-if="!task.completed"
@click.stop="copyTaskCommand(spec.name, task.id, $event)"
class="text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300 flex items-center gap-1"
:title="`Copy command: /spec-execute ${spec.name} ${task.id}`"
>
<i class="fas fa-copy"></i>
<span>/spec-execute</span>
</button>
<span
v-if="task.requirements && task.requirements.length > 0"
class="text-xs text-gray-500 dark:text-gray-400"
>
Req: {{ task.requirements.join(', ') }}
</span>
<span
v-if="task.leverage"
class="text-xs text-gray-500 dark:text-gray-400"
>
<i class="fas fa-screwdriver mr-1"></i> {{ task.leverage }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bugs Section -->
<div v-if="selectedProject.bugs && selectedProject.bugs.length > 0" class="mt-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<i class="fas fa-bug mr-2"></i>Bug Tracking
</h2>
<div class="bg-white dark:bg-gray-800 shadow rounded-lg">
<div class="divide-y divide-gray-200 dark:divide-gray-700">
<div
v-for="bug in selectedProject.bugs"
:key="bug.name"
class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-all p-4"
>
<div class="flex items-start justify-between">
<div class="flex-1">
<h3 class="text-base font-medium text-gray-900 dark:text-white">
{{ bug.displayName }}
</h3>
<!-- Bug Status -->
<div class="mt-2 flex items-center gap-4 text-sm">
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200': bug.status === 'reported',
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200': bug.status === 'analyzing',
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200': bug.status === 'fixing',
'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200': bug.status === 'verifying',
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200': bug.status === 'resolved'
}"
>
{{ bug.status }}
</span>
<span v-if="bug.report?.severity"
class="inline-flex items-center text-xs"
:class="{
'text-red-600 dark:text-red-400': bug.report.severity === 'critical',
'text-orange-600 dark:text-orange-400': bug.report.severity === 'high',
'text-yellow-600 dark:text-yellow-400': bug.report.severity === 'medium',
'text-gray-600 dark:text-gray-