spec-feature 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/viewer.html CHANGED
@@ -7,6 +7,11 @@
7
7
 
8
8
  <!-- Tailwind CSS -->
9
9
  <script src="https://cdn.tailwindcss.com"></script>
10
+ <script>
11
+ tailwind.config = {
12
+ darkMode: 'class',
13
+ }
14
+ </script>
10
15
 
11
16
  <!-- React & ReactDOM -->
12
17
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
@@ -29,9 +34,13 @@
29
34
  ::-webkit-scrollbar-track { background: transparent; }
30
35
  ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
31
36
  ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
37
+
38
+ /* Dark mode scrollbar */
39
+ .dark ::-webkit-scrollbar-thumb { background: #475569; }
40
+ .dark ::-webkit-scrollbar-thumb:hover { background: #64748b; }
32
41
  </style>
33
42
  </head>
34
- <body class="bg-slate-50 text-slate-900 overflow-hidden">
43
+ <body class="bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-100 overflow-hidden">
35
44
  <div id="root"></div>
36
45
  <!-- spec-feature:config:inject -->
37
46
 
@@ -62,6 +71,8 @@
62
71
  const ExternalLink = (p) => <IconBase {...p}><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" x2="21" y1="14" y2="3" /></IconBase>;
63
72
  const Grid = (p) => <IconBase {...p}><rect width="7" height="7" x="3" y="3" rx="1" /><rect width="7" height="7" x="14" y="3" rx="1" /><rect width="7" height="7" x="14" y="14" rx="1" /><rect width="7" height="7" x="3" y="14" rx="1" /></IconBase>;
64
73
  const FolderGit = (p) => <IconBase {...p}><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 2H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2Z"/><circle cx="12" cy="13" r="2"/><path d="M12 10v1"/><path d="M12 15v2"/></IconBase>;
74
+ const Sun = (p) => <IconBase {...p}><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></IconBase>;
75
+ const Moon = (p) => <IconBase {...p}><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></IconBase>;
65
76
 
66
77
  // --- DATA FETCHING ---
67
78
  const fetchFeatures = async () => {
@@ -91,26 +102,35 @@
91
102
  // --- COMPONENTS ---
92
103
 
93
104
  const MermaidBlock = ({ content }) => (
94
- <div className="my-6 border border-slate-200 rounded-lg bg-slate-50 overflow-hidden">
95
- <div className="bg-slate-100 px-4 py-2 border-b border-slate-200 flex items-center gap-2 text-xs font-mono text-slate-500">
96
- <GitGraph size={14} className="text-blue-500" />
105
+ <div className="my-6 border border-slate-200 dark:border-slate-700 rounded-lg bg-slate-50 dark:bg-slate-800 overflow-hidden">
106
+ <div className="bg-slate-100 dark:bg-slate-700 px-4 py-2 border-b border-slate-200 dark:border-slate-600 flex items-center gap-2 text-xs font-mono text-slate-500 dark:text-slate-400">
107
+ <GitGraph size={14} className="text-blue-500 dark:text-blue-400" />
97
108
  <span>Mermaid Diagram</span>
98
109
  </div>
99
- <div className="p-4 font-mono text-xs text-slate-600 bg-white whitespace-pre overflow-x-auto">
110
+ <div className="p-4 font-mono text-xs text-slate-600 dark:text-slate-300 bg-white dark:bg-slate-800 whitespace-pre overflow-x-auto">
100
111
  {content}
101
112
  </div>
102
- <div className="bg-slate-50 px-4 py-2 text-[10px] text-slate-400 border-t border-slate-200 text-center">
113
+ <div className="bg-slate-50 dark:bg-slate-800 px-4 py-2 text-[10px] text-slate-400 dark:text-slate-500 border-t border-slate-200 dark:border-slate-700 text-center">
103
114
  Diagram visualization would render here
104
115
  </div>
105
116
  </div>
106
117
  );
107
118
 
119
+ const renderBoldText = (text) => {
120
+ const parts = text.split('**');
121
+ return parts.map((part, idx) =>
122
+ idx % 2 === 1 ? <strong key={idx} className="font-semibold text-slate-900 dark:text-slate-100">{part}</strong> : part
123
+ );
124
+ };
125
+
108
126
  const LinkRenderer = ({ text, onNavigate }) => {
109
127
  const fileRegex = /(spec\.md|plan\.md|tasks\.md|verify-report\.md)/g;
110
128
  const parts = text.split(fileRegex);
111
- if (parts.length === 1) return <span className="text-slate-600">{text}</span>;
129
+ if (parts.length === 1) {
130
+ return <span className="text-slate-600 dark:text-slate-400">{renderBoldText(text)}</span>;
131
+ }
112
132
  return (
113
- <span className="text-slate-600">
133
+ <span className="text-slate-600 dark:text-slate-400">
114
134
  {parts.map((part, i) => {
115
135
  if (part.match(fileRegex)) {
116
136
  let targetId = 'spec';
@@ -118,12 +138,12 @@
118
138
  if (part.includes('tasks')) targetId = 'tasks';
119
139
  if (part.includes('verify')) targetId = 'verify';
120
140
  return (
121
- <button key={i} onClick={() => onNavigate(targetId)} className="inline-flex items-center gap-0.5 px-1.5 py-0.5 -my-1 mx-0.5 text-blue-600 bg-blue-50 hover:bg-blue-100 rounded text-sm font-medium transition-colors cursor-pointer" title={`Go to ${part}`}>
141
+ <button key={i} onClick={() => onNavigate(targetId)} className="inline-flex items-center gap-0.5 px-1.5 py-0.5 -my-1 mx-0.5 text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/50 rounded text-sm font-medium transition-colors cursor-pointer" title={`Go to ${part}`}>
122
142
  {part} <ExternalLink size={10} />
123
143
  </button>
124
144
  );
125
145
  }
126
- return part;
146
+ return renderBoldText(part);
127
147
  })}
128
148
  </span>
129
149
  );
@@ -143,7 +163,7 @@
143
163
  if (codeBlockLang === 'mermaid') {
144
164
  renderLines.push(<MermaidBlock key={`mermaid-${i}`} content={codeBlockContent.join('\n')} />);
145
165
  } else {
146
- renderLines.push(<div key={`code-${i}`} className="my-4 p-4 bg-slate-100 rounded-md font-mono text-sm text-slate-700 overflow-x-auto border border-slate-200">{codeBlockContent.join('\n')}</div>);
166
+ renderLines.push(<div key={`code-${i}`} className="my-4 p-4 bg-slate-100 dark:bg-slate-800 rounded-md font-mono text-sm text-slate-700 dark:text-slate-300 overflow-x-auto border border-slate-200 dark:border-slate-700">{codeBlockContent.join('\n')}</div>);
147
167
  }
148
168
  inCodeBlock = false; codeBlockContent = []; codeBlockLang = '';
149
169
  } else {
@@ -152,25 +172,51 @@
152
172
  continue;
153
173
  }
154
174
  if (inCodeBlock) { codeBlockContent.push(line); continue; }
155
- if (line.startsWith('# ')) renderLines.push(<h1 key={i} className="text-2xl font-bold text-slate-900 mb-6 pb-2 border-b border-slate-200">{line.replace('# ', '')}</h1>);
156
- else if (line.startsWith('## ')) renderLines.push(<h2 key={i} className="text-lg font-bold text-slate-800 mt-8 mb-3 flex items-center gap-2">{line.replace('## ', '')}</h2>);
157
- else if (line.startsWith('### ')) renderLines.push(<h3 key={i} className="text-md font-semibold text-slate-700 mt-6 mb-2">{line.replace('### ', '')}</h3>);
175
+ if (line.startsWith('# ')) {
176
+ const h1Text = line.replace('# ', '');
177
+ renderLines.push(<h1 key={i} className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-6 pb-2 border-b border-slate-200 dark:border-slate-700">{renderBoldText(h1Text)}</h1>);
178
+ }
179
+ else if (line.startsWith('## ')) {
180
+ const h2Text = line.replace('## ', '');
181
+ renderLines.push(<h2 key={i} className="text-lg font-bold text-slate-800 dark:text-slate-200 mt-8 mb-3 flex items-center gap-2">{renderBoldText(h2Text)}</h2>);
182
+ }
183
+ else if (line.startsWith('### ')) {
184
+ const h3Text = line.replace('### ', '');
185
+ renderLines.push(<h3 key={i} className="text-md font-semibold text-slate-700 dark:text-slate-300 mt-6 mb-2">{renderBoldText(h3Text)}</h3>);
186
+ }
158
187
  else if (line.trim().startsWith('- [ ]') || line.trim().startsWith('- [x]')) {
159
188
  const isChecked = line.trim().startsWith('- [x]');
160
189
  const text = line.replace(/- \[[x ]\] /, '');
161
190
  renderLines.push(
162
191
  <div key={i} className="flex items-start gap-3 py-1 ml-1 group">
163
- <div className={`mt-1 w-4 h-4 rounded border flex items-center justify-center shrink-0 ${isChecked ? 'bg-green-500 border-green-500 text-white' : 'border-slate-300 bg-white'}`}>
192
+ <div className={`mt-1 w-4 h-4 rounded border flex items-center justify-center shrink-0 ${isChecked ? 'bg-green-500 dark:bg-green-600 border-green-500 dark:border-green-600 text-white' : 'border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800'}`}>
164
193
  {isChecked && <Check size={12} strokeWidth={3} />}
165
194
  </div>
166
- <span className={`${isChecked ? 'text-slate-400 line-through' : 'text-slate-700'}`}>
167
- {text.split('**').map((part, idx) => idx % 2 === 1 ? <strong key={idx} className="font-semibold text-slate-900">{part}</strong> : part)}
195
+ <span className={`${isChecked ? 'text-slate-400 dark:text-slate-500 line-through' : 'text-slate-700 dark:text-slate-300'}`}>
196
+ {renderBoldText(text)}
168
197
  </span>
169
198
  </div>
170
199
  );
171
200
  }
172
- else if (line.startsWith('- ')) renderLines.push(<li key={i} className="ml-5 list-disc text-slate-700 pl-1">{line.replace('- ', '').replace(/\*\*(.*?)\*\*/g, (match, p1) => p1)}</li>);
173
- else if (/^\d+\. /.test(line)) renderLines.push(<div key={i} className="ml-5 text-slate-700 flex gap-2"><span className="font-mono text-slate-400 font-bold w-4 text-right">{line.split('.')[0]}.</span> {line.replace(/^\d+\. /, '')}</div>);
201
+ else if (line.startsWith('- ')) {
202
+ const listText = line.replace('- ', '');
203
+ renderLines.push(
204
+ <li key={i} className="ml-5 list-disc text-slate-700 dark:text-slate-300 pl-1">
205
+ {renderBoldText(listText)}
206
+ </li>
207
+ );
208
+ }
209
+ else if (/^\d+\. /.test(line)) {
210
+ const numMatch = line.match(/^(\d+)\. (.*)/);
211
+ if (numMatch) {
212
+ renderLines.push(
213
+ <div key={i} className="ml-5 text-slate-700 dark:text-slate-300 flex gap-2">
214
+ <span className="font-mono text-slate-400 dark:text-slate-500 font-bold w-4 text-right">{numMatch[1]}.</span>
215
+ <span>{renderBoldText(numMatch[2])}</span>
216
+ </div>
217
+ );
218
+ }
219
+ }
174
220
  else if (line.trim() === '') renderLines.push(<div key={i} className="h-2"></div>);
175
221
  else renderLines.push(<p key={i} className="leading-relaxed"><LinkRenderer text={line} onNavigate={onNavigate} /></p>);
176
222
  }
@@ -188,16 +234,16 @@
188
234
  };
189
235
 
190
236
  const SidebarItem = ({ file, isActive, onClick }) => (
191
- <button onClick={onClick} className={`w-full flex items-center gap-3 px-4 py-3.5 text-sm transition-all border-l-2 text-left group ${isActive ? 'bg-white border-blue-600 shadow-sm' : 'border-transparent hover:bg-slate-100 hover:text-slate-900'}`}>
192
- <div className={`p-1.5 rounded-md ${isActive ? 'bg-blue-50' : 'bg-white group-hover:bg-white'}`}>
237
+ <button onClick={onClick} className={`w-full flex items-center gap-3 px-4 py-3.5 text-sm transition-all border-l-2 text-left group ${isActive ? 'bg-white dark:bg-slate-800 border-blue-600 dark:border-blue-500 shadow-sm' : 'border-transparent hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-900 dark:hover:text-slate-100'}`}>
238
+ <div className={`p-1.5 rounded-md ${isActive ? 'bg-blue-50 dark:bg-blue-900/30' : 'bg-white dark:bg-slate-800 group-hover:bg-white dark:group-hover:bg-slate-800'}`}>
193
239
  <FileIcon type={file.type} />
194
240
  </div>
195
241
  <div className="flex flex-col overflow-hidden w-full">
196
242
  <div className="flex justify-between items-center w-full">
197
- <span className={`font-medium truncate ${isActive ? 'text-slate-900' : 'text-slate-600'}`}>{file.name}</span>
198
- {isActive && <ChevronRight size={14} className="text-blue-500 shrink-0" />}
243
+ <span className={`font-medium truncate ${isActive ? 'text-slate-900 dark:text-slate-100' : 'text-slate-600 dark:text-slate-400'}`}>{file.name}</span>
244
+ {isActive && <ChevronRight size={14} className="text-blue-500 dark:text-blue-400 shrink-0" />}
199
245
  </div>
200
- <span className="text-[10px] text-slate-400 uppercase tracking-wider font-semibold truncate">{file.description}</span>
246
+ <span className="text-[10px] text-slate-400 dark:text-slate-500 uppercase tracking-wider font-semibold truncate">{file.description}</span>
201
247
  </div>
202
248
  </button>
203
249
  );
@@ -207,35 +253,35 @@
207
253
  return (
208
254
  <div className="w-full">
209
255
  <div className="flex justify-between text-xs mb-1.5">
210
- <span className={`font-semibold ${percentage === 100 ? 'text-green-600' : 'text-slate-600'}`}>
256
+ <span className={`font-semibold ${percentage === 100 ? 'text-green-600 dark:text-green-400' : 'text-slate-600 dark:text-slate-400'}`}>
211
257
  {percentage === 100 ? 'Done' : `${percentage}%`}
212
258
  </span>
213
- <span className="text-slate-400">{completed}/{total}</span>
259
+ <span className="text-slate-400 dark:text-slate-500">{completed}/{total}</span>
214
260
  </div>
215
- <div className="h-1.5 w-full bg-slate-200 rounded-full overflow-hidden">
216
- <div className={`h-full rounded-full ${percentage === 100 ? 'bg-green-500' : 'bg-amber-500'}`} style={{ width: `${percentage}%` }}></div>
261
+ <div className="h-1.5 w-full bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
262
+ <div className={`h-full rounded-full ${percentage === 100 ? 'bg-green-500 dark:bg-green-600' : 'bg-amber-500 dark:bg-amber-600'}`} style={{ width: `${percentage}%` }}></div>
217
263
  </div>
218
264
  </div>
219
265
  );
220
266
  }
221
267
 
222
268
  return (
223
- <button onClick={onClick} className="w-full text-left bg-slate-50 rounded-lg p-3 border border-slate-200 shadow-sm cursor-pointer hover:bg-white hover:border-blue-300 hover:shadow-md transition-all group" title="Go to Tasks Checklist">
269
+ <button onClick={onClick} className="w-full text-left bg-slate-50 dark:bg-slate-800 rounded-lg p-3 border border-slate-200 dark:border-slate-700 shadow-sm cursor-pointer hover:bg-white dark:hover:bg-slate-700 hover:border-blue-300 dark:hover:border-blue-600 hover:shadow-md transition-all group" title="Go to Tasks Checklist">
224
270
  <div className="flex justify-between items-end mb-2">
225
271
  <div className="flex flex-col">
226
- <div className="text-xs font-medium text-slate-500 mb-0.5 group-hover:text-blue-600 transition-colors">Feature Status</div>
227
- <div className="flex items-center gap-2 text-sm font-bold text-slate-800">
272
+ <div className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-0.5 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">Feature Status</div>
273
+ <div className="flex items-center gap-2 text-sm font-bold text-slate-800 dark:text-slate-200">
228
274
  {percentage === 100 ? (
229
- <span className="flex items-center gap-1.5 text-green-600"><Check size={14} strokeWidth={3}/> Ready to Verify</span>
275
+ <span className="flex items-center gap-1.5 text-green-600 dark:text-green-400"><Check size={14} strokeWidth={3}/> Ready to Verify</span>
230
276
  ) : (
231
- <span className="flex items-center gap-1.5 text-amber-600"><div className="w-2 h-2 rounded-full bg-amber-500 animate-pulse" /> In Progress</span>
277
+ <span className="flex items-center gap-1.5 text-amber-600 dark:text-amber-500"><div className="w-2 h-2 rounded-full bg-amber-500 dark:bg-amber-400 animate-pulse" /> In Progress</span>
232
278
  )}
233
279
  </div>
234
280
  </div>
235
- <div className="text-xs font-mono font-medium text-slate-400 group-hover:text-slate-600">{completed}/{total}</div>
281
+ <div className="text-xs font-mono font-medium text-slate-400 dark:text-slate-500 group-hover:text-slate-600 dark:group-hover:text-slate-400">{completed}/{total}</div>
236
282
  </div>
237
- <div className="h-1.5 w-full bg-slate-200 rounded-full overflow-hidden">
238
- <div className={`h-full transition-all duration-500 ease-out rounded-full ${percentage === 100 ? 'bg-green-500' : 'bg-amber-500'}`} style={{ width: `${percentage}%` }} />
283
+ <div className="h-1.5 w-full bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
284
+ <div className={`h-full transition-all duration-500 ease-out rounded-full ${percentage === 100 ? 'bg-green-500 dark:bg-green-600' : 'bg-amber-500 dark:bg-amber-600'}`} style={{ width: `${percentage}%` }} />
239
285
  </div>
240
286
  </button>
241
287
  );
@@ -245,37 +291,37 @@
245
291
  return (
246
292
  <div className="p-8 max-w-6xl mx-auto">
247
293
  <div className="flex items-center gap-3 mb-8">
248
- <div className="p-2 bg-indigo-600 rounded-lg text-white"><FolderGit size={24} /></div>
294
+ <div className="p-2 bg-indigo-600 dark:bg-indigo-500 rounded-lg text-white"><FolderGit size={24} /></div>
249
295
  <div>
250
- <h1 className="text-2xl font-bold text-slate-900">Project Specifications</h1>
251
- <p className="text-slate-500 text-sm">Overview of all active features and their implementation status</p>
296
+ <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Project Specifications</h1>
297
+ <p className="text-slate-500 dark:text-slate-400 text-sm">Overview of all active features and their implementation status</p>
252
298
  </div>
253
299
  </div>
254
300
 
255
301
  {features.length === 0 ? (
256
- <div className="text-slate-500 text-sm">No features found.</div>
302
+ <div className="text-slate-500 dark:text-slate-400 text-sm">No features found.</div>
257
303
  ) : (
258
304
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
259
305
  {features.map(feature => {
260
306
  const progress = calculateProgress(feature.files);
261
307
  return (
262
308
  <div key={feature.id} onClick={() => onSelectFeature(feature.id)}
263
- className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm hover:shadow-md hover:border-blue-300 transition-all cursor-pointer group flex flex-col h-full">
309
+ className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-5 shadow-sm hover:shadow-md hover:border-blue-300 dark:hover:border-blue-600 transition-all cursor-pointer group flex flex-col h-full">
264
310
  <div className="flex items-start justify-between mb-4">
265
311
  <div className="flex items-center gap-2">
266
- <span className="text-slate-400 text-sm font-mono">#</span>
267
- <h3 className="font-bold text-slate-800 group-hover:text-blue-600 transition-colors">{feature.title}</h3>
312
+ <span className="text-slate-400 dark:text-slate-500 text-sm font-mono">#</span>
313
+ <h3 className="font-bold text-slate-800 dark:text-slate-200 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">{feature.title}</h3>
268
314
  </div>
269
- {progress.percentage === 100 && <div className="text-green-500"><ShieldCheck size={18} /></div>}
315
+ {progress.percentage === 100 && <div className="text-green-500 dark:text-green-400"><ShieldCheck size={18} /></div>}
270
316
  </div>
271
317
 
272
318
  <div className="flex-1">
273
- <p className="text-xs text-slate-500 mb-4 line-clamp-2">
319
+ <p className="text-xs text-slate-500 dark:text-slate-400 mb-4 line-clamp-2">
274
320
  {feature.files.find(f => f.type === 'spec')?.content.slice(0, 100).replace(/#/g, '') || 'No description'}...
275
321
  </p>
276
322
  </div>
277
323
 
278
- <div className="mt-auto pt-4 border-t border-slate-100">
324
+ <div className="mt-auto pt-4 border-t border-slate-100 dark:border-slate-700">
279
325
  <ProgressBar
280
326
  minimal
281
327
  percentage={progress.percentage}
@@ -303,8 +349,22 @@
303
349
  const [selectedFileId, setSelectedFileId] = useState('spec');
304
350
  const [isSidebarOpen, setSidebarOpen] = useState(true);
305
351
  const [viewMode, setViewMode] = useState('preview');
352
+ const [isDark, setIsDark] = useState(() => {
353
+ const saved = localStorage.getItem('theme');
354
+ if (saved) return saved === 'dark';
355
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
356
+ });
306
357
  const initializedWithoutFeatures = CONFIG.isInitialized && !CONFIG.hasFeatures;
307
358
 
359
+ useEffect(() => {
360
+ if (isDark) {
361
+ document.documentElement.classList.add('dark');
362
+ } else {
363
+ document.documentElement.classList.remove('dark');
364
+ }
365
+ localStorage.setItem('theme', isDark ? 'dark' : 'light');
366
+ }, [isDark]);
367
+
308
368
  useEffect(() => {
309
369
  fetchFeatures()
310
370
  .then(data => {
@@ -358,39 +418,44 @@
358
418
  // --- VIEW: DASHBOARD ---
359
419
  if (!activeFeatureId) {
360
420
  return (
361
- <div className="min-h-screen bg-slate-50 font-sans text-slate-900 overflow-auto">
421
+ <div className="min-h-screen bg-slate-50 dark:bg-slate-900 font-sans text-slate-900 dark:text-slate-100 overflow-auto">
422
+ <div className="fixed top-4 right-4 z-50">
423
+ <button onClick={() => setIsDark(!isDark)} className="p-2.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-sm hover:shadow-md transition-all text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100" title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}>
424
+ {isDark ? <Sun size={20} /> : <Moon size={20} />}
425
+ </button>
426
+ </div>
362
427
  {loading ? (
363
- <div className="p-8 max-w-6xl mx-auto text-slate-500">Loading features...</div>
428
+ <div className="p-8 max-w-6xl mx-auto text-slate-500 dark:text-slate-400">Loading features...</div>
364
429
  ) : error ? (
365
430
  <div className="p-8 max-w-6xl mx-auto">
366
- <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
431
+ <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-lg">
367
432
  {error}
368
433
  </div>
369
434
  </div>
370
435
  ) : features.length === 0 ? (
371
436
  <div className="p-8 max-w-5xl mx-auto space-y-6">
372
437
  <div className="flex items-center gap-3">
373
- <div className="p-2 bg-indigo-600 rounded-lg text-white"><FolderGit size={24} /></div>
438
+ <div className="p-2 bg-indigo-600 dark:bg-indigo-500 rounded-lg text-white"><FolderGit size={24} /></div>
374
439
  <div>
375
- <h1 className="text-2xl font-bold text-slate-900">Project Specifications</h1>
376
- <p className="text-slate-500 text-sm">Overview of all active features and their implementation status</p>
440
+ <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Project Specifications</h1>
441
+ <p className="text-slate-500 dark:text-slate-400 text-sm">Overview of all active features and their implementation status</p>
377
442
  </div>
378
443
  </div>
379
444
  {initializedWithoutFeatures ? (
380
- <div className="bg-white border border-slate-200 rounded-xl p-6 text-slate-600 shadow-sm space-y-4">
381
- <div className="font-semibold text-slate-900">No features found. Create your first specification:</div>
445
+ <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-6 text-slate-600 dark:text-slate-300 shadow-sm space-y-4">
446
+ <div className="font-semibold text-slate-900 dark:text-slate-100">No features found. Create your first specification:</div>
382
447
  <div className="space-y-2">
383
448
  <div className="text-sm">1. Open your AI assistant (Claude, Cursor, Copilot, etc.)</div>
384
449
  <div className="text-sm">2. Copy and paste this prompt:</div>
385
- <pre className="bg-slate-50 border border-slate-200 rounded-md p-3 text-sm text-slate-700 overflow-auto font-mono">Use the template from spec/feature.md.
450
+ <pre className="bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-md p-3 text-sm text-slate-700 dark:text-slate-300 overflow-auto font-mono">Use the template from spec/feature.md.
386
451
  #feature-name# Your description here</pre>
387
- <div className="text-sm">Replace <code className="bg-slate-100 px-1 py-0.5 rounded">feature-name</code> with your feature slug (e.g., <code className="bg-slate-100 px-1 py-0.5 rounded">user-auth</code>) and describe what you want to build.</div>
388
- <div className="text-sm">3. The AI will create <code className="bg-slate-100 px-1 py-0.5 rounded">spec/features/&lt;feature-name&gt;/</code> with <code className="bg-slate-100 px-1 py-0.5 rounded">spec.md</code>, <code className="bg-slate-100 px-1 py-0.5 rounded">plan.md</code>, and <code className="bg-slate-100 px-1 py-0.5 rounded">tasks.md</code></div>
389
- <div className="text-sm font-medium text-slate-700">4. Refresh this page to see your new feature</div>
452
+ <div className="text-sm">Replace <code className="bg-slate-100 dark:bg-slate-700 px-1 py-0.5 rounded">feature-name</code> with your feature slug (e.g., <code className="bg-slate-100 dark:bg-slate-700 px-1 py-0.5 rounded">user-auth</code>) and describe what you want to build.</div>
453
+ <div className="text-sm">3. The AI will create <code className="bg-slate-100 dark:bg-slate-700 px-1 py-0.5 rounded">spec/features/&lt;feature-name&gt;/</code> with <code className="bg-slate-100 dark:bg-slate-700 px-1 py-0.5 rounded">spec.md</code>, <code className="bg-slate-100 dark:bg-slate-700 px-1 py-0.5 rounded">plan.md</code>, and <code className="bg-slate-100 dark:bg-slate-700 px-1 py-0.5 rounded">tasks.md</code></div>
454
+ <div className="text-sm font-medium text-slate-700 dark:text-slate-300">4. Refresh this page to see your new feature</div>
390
455
  </div>
391
456
  </div>
392
457
  ) : (
393
- <div className="bg-white border border-slate-200 rounded-xl p-6 text-slate-600 shadow-sm">
458
+ <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-6 text-slate-600 dark:text-slate-300 shadow-sm">
394
459
  No features found in folder "{CONFIG.folderName}". Use --folder to point to a valid specs directory.
395
460
  </div>
396
461
  )}
@@ -405,11 +470,11 @@
405
470
  // --- VIEW: FEATURE DETAILS (Legacy View) ---
406
471
  if (!activeFeature) {
407
472
  return (
408
- <div className="min-h-screen bg-slate-50 p-10 text-slate-600">
409
- <div className="max-w-3xl mx-auto bg-white border border-slate-200 rounded-xl p-6 shadow-sm">
473
+ <div className="min-h-screen bg-slate-50 dark:bg-slate-900 p-10 text-slate-600 dark:text-slate-400">
474
+ <div className="max-w-3xl mx-auto bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-6 shadow-sm">
410
475
  Feature not found. Please go back to dashboard.
411
476
  <div className="mt-4">
412
- <button onClick={handleBackToDashboard} className="px-4 py-2 bg-blue-600 text-white rounded-md text-sm">Back</button>
477
+ <button onClick={handleBackToDashboard} className="px-4 py-2 bg-blue-600 dark:bg-blue-500 text-white rounded-md text-sm hover:bg-blue-700 dark:hover:bg-blue-600 transition-colors">Back</button>
413
478
  </div>
414
479
  </div>
415
480
  </div>
@@ -417,35 +482,35 @@
417
482
  }
418
483
 
419
484
  return (
420
- <div className="flex h-screen bg-slate-50 overflow-hidden font-sans text-slate-900">
485
+ <div className="flex h-screen bg-slate-50 dark:bg-slate-900 overflow-hidden font-sans text-slate-900 dark:text-slate-100">
421
486
 
422
487
  {/* Mobile Sidebar Overlay */}
423
488
  {!isSidebarOpen && (
424
- <button onClick={() => setSidebarOpen(true)} className="lg:hidden absolute top-4 left-4 z-50 p-2 bg-white rounded-md shadow-md border border-slate-200 text-slate-600">
489
+ <button onClick={() => setSidebarOpen(true)} className="lg:hidden absolute top-4 left-4 z-50 p-2 bg-white dark:bg-slate-800 rounded-md shadow-md border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400">
425
490
  <MenuIcon size={20} />
426
491
  </button>
427
492
  )}
428
493
 
429
494
  {/* Sidebar */}
430
- <aside className={`fixed inset-y-0 left-0 z-40 w-80 bg-slate-50 border-r border-slate-200 transform transition-transform duration-200 ease-in-out lg:relative lg:translate-x-0 ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`}>
495
+ <aside className={`fixed inset-y-0 left-0 z-40 w-80 bg-slate-50 dark:bg-slate-900 border-r border-slate-200 dark:border-slate-700 transform transition-transform duration-200 ease-in-out lg:relative lg:translate-x-0 ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`}>
431
496
  <div className="flex flex-col h-full">
432
497
 
433
498
  {/* Header: Back Button */}
434
- <div className="h-16 flex items-center px-4 border-b border-slate-200 bg-white">
435
- <button onClick={handleBackToDashboard} className="flex items-center gap-2 text-sm font-semibold text-slate-600 hover:text-blue-600 transition-colors w-full">
436
- <div className="p-1.5 rounded-md bg-slate-100 group-hover:bg-blue-50">
499
+ <div className="h-16 flex items-center px-4 border-b border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800">
500
+ <button onClick={handleBackToDashboard} className="flex items-center gap-2 text-sm font-semibold text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors w-full">
501
+ <div className="p-1.5 rounded-md bg-slate-100 dark:bg-slate-700 group-hover:bg-blue-50 dark:group-hover:bg-blue-900/30">
437
502
  <ChevronLeft size={16} />
438
503
  </div>
439
504
  Back to Features
440
505
  </button>
441
- <button onClick={() => setSidebarOpen(false)} className="lg:hidden ml-auto p-1 text-slate-400 hover:text-slate-600"><X size={20} /></button>
506
+ <button onClick={() => setSidebarOpen(false)} className="lg:hidden ml-auto p-1 text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300"><X size={20} /></button>
442
507
  </div>
443
508
 
444
509
  {/* Active Feature Context */}
445
- <div className="px-5 py-4 bg-slate-50/50">
446
- <div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1.5">Active Feature</div>
447
- <div className="flex items-center gap-2 text-slate-800 font-mono text-sm font-semibold bg-white border border-slate-200 px-3 py-2 rounded-md shadow-sm truncate">
448
- <span className="text-blue-500">#</span> <span className="truncate">{activeFeature.title}</span>
510
+ <div className="px-5 py-4 bg-slate-50/50 dark:bg-slate-900/50">
511
+ <div className="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider mb-1.5">Active Feature</div>
512
+ <div className="flex items-center gap-2 text-slate-800 dark:text-slate-200 font-mono text-sm font-semibold bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 px-3 py-2 rounded-md shadow-sm truncate">
513
+ <span className="text-blue-500 dark:text-blue-400">#</span> <span className="truncate">{activeFeature.title}</span>
449
514
  </div>
450
515
  </div>
451
516
 
@@ -459,42 +524,45 @@
459
524
  </div>
460
525
 
461
526
  {/* Footer Progress */}
462
- <div className="p-4 border-t border-slate-200 bg-white">
527
+ <div className="p-4 border-t border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800">
463
528
  <ProgressBar percentage={stats.percentage} total={stats.total} completed={stats.completed} onClick={() => handleNavigateFile('tasks')} />
464
529
  </div>
465
530
  </div>
466
531
  </aside>
467
532
 
468
533
  {/* Main Content */}
469
- <main className="flex-1 flex flex-col h-full min-w-0 bg-white relative">
534
+ <main className="flex-1 flex flex-col h-full min-w-0 bg-white dark:bg-slate-900 relative">
470
535
  {/* Header */}
471
- <header className="h-16 flex items-center justify-between px-6 border-b border-slate-100 bg-white shrink-0 z-10">
536
+ <header className="h-16 flex items-center justify-between px-6 border-b border-slate-100 dark:border-slate-700 bg-white dark:bg-slate-800 shrink-0 z-10">
472
537
  <div className="flex items-center gap-4 overflow-hidden">
473
- <button onClick={() => setSidebarOpen(!isSidebarOpen)} className="hidden lg:flex p-2 text-slate-400 hover:text-slate-600 rounded-md hover:bg-slate-50 transition-colors"><MenuIcon size={18} /></button>
474
- <h1 className="text-lg font-bold text-slate-800 truncate">{selectedFile.name}</h1>
538
+ <button onClick={() => setSidebarOpen(!isSidebarOpen)} className="hidden lg:flex p-2 text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300 rounded-md hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"><MenuIcon size={18} /></button>
539
+ <h1 className="text-lg font-bold text-slate-800 dark:text-slate-200 truncate">{selectedFile.name}</h1>
475
540
  </div>
476
541
  <div className="flex items-center gap-3 shrink-0">
477
- <div className="flex bg-slate-100/80 p-1 rounded-lg border border-slate-200">
478
- <button onClick={() => setViewMode('preview')} className={`flex items-center gap-2 px-3 py-1.5 text-xs font-bold rounded-md transition-all ${viewMode === 'preview' ? 'bg-white text-slate-800 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}><Eye size={14} /> Preview</button>
479
- <button onClick={() => setViewMode('raw')} className={`flex items-center gap-2 px-3 py-1.5 text-xs font-bold rounded-md transition-all ${viewMode === 'raw' ? 'bg-white text-slate-800 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}><CodeIcon size={14} /> Raw</button>
542
+ <button onClick={() => setIsDark(!isDark)} className="p-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-all" title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}>
543
+ {isDark ? <Sun size={18} /> : <Moon size={18} />}
544
+ </button>
545
+ <div className="flex bg-slate-100/80 dark:bg-slate-800 p-1 rounded-lg border border-slate-200 dark:border-slate-700">
546
+ <button onClick={() => setViewMode('preview')} className={`flex items-center gap-2 px-3 py-1.5 text-xs font-bold rounded-md transition-all ${viewMode === 'preview' ? 'bg-white dark:bg-slate-700 text-slate-800 dark:text-slate-200 shadow-sm' : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'}`}><Eye size={14} /> Preview</button>
547
+ <button onClick={() => setViewMode('raw')} className={`flex items-center gap-2 px-3 py-1.5 text-xs font-bold rounded-md transition-all ${viewMode === 'raw' ? 'bg-white dark:bg-slate-700 text-slate-800 dark:text-slate-200 shadow-sm' : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'}`}><CodeIcon size={14} /> Raw</button>
480
548
  </div>
481
- <div className="w-px h-6 bg-slate-200 mx-1"></div>
549
+ <div className="w-px h-6 bg-slate-200 dark:bg-slate-700 mx-1"></div>
482
550
  {/* Copy Button Logic embedded here for brevity */}
483
551
  <button onClick={() => {
484
552
  const t = document.createElement("textarea"); t.value = selectedFile.content; document.body.appendChild(t); t.select(); document.execCommand('copy'); document.body.removeChild(t);
485
- }} className="flex items-center gap-2 px-3 py-1.5 text-xs font-semibold text-slate-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all border border-slate-200 hover:border-blue-200 shadow-sm"><Copy size={14} /> Copy</button>
553
+ }} className="flex items-center gap-2 px-3 py-1.5 text-xs font-semibold text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded-lg transition-all border border-slate-200 dark:border-slate-700 hover:border-blue-200 dark:hover:border-blue-700 shadow-sm"><Copy size={14} /> Copy</button>
486
554
  </div>
487
555
  </header>
488
556
 
489
557
  {/* Content */}
490
- <div className="flex-1 overflow-auto bg-slate-50/30 scroll-smooth">
558
+ <div className="flex-1 overflow-auto bg-slate-50/30 dark:bg-slate-900 scroll-smooth">
491
559
  <div className="max-w-5xl mx-auto my-8 px-6 lg:px-10 pb-20">
492
- <div className="bg-white rounded-xl shadow-[0_2px_12px_-4px_rgba(0,0,0,0.05)] border border-slate-200 overflow-hidden min-h-[70vh]">
560
+ <div className="bg-white dark:bg-slate-800 rounded-xl shadow-[0_2px_12px_-4px_rgba(0,0,0,0.05)] dark:shadow-[0_2px_12px_-4px_rgba(0,0,0,0.3)] border border-slate-200 dark:border-slate-700 overflow-hidden min-h-[70vh]">
493
561
  {viewMode === 'preview' ? (
494
562
  <div className="p-10 lg:p-14"><SmartMarkdownRenderer content={selectedFile.content} onNavigate={handleNavigateFile} /></div>
495
563
  ) : (
496
- <div className="bg-[#1e293b] h-full min-h-[70vh] flex flex-col">
497
- <div className="bg-[#0f172a] px-4 py-2 text-xs text-slate-400 font-mono border-b border-slate-700 flex justify-between"><span>markdown</span><span>utf-8</span></div>
564
+ <div className="bg-[#1e293b] dark:bg-[#0f172a] h-full min-h-[70vh] flex flex-col">
565
+ <div className="bg-[#0f172a] dark:bg-[#020617] px-4 py-2 text-xs text-slate-400 font-mono border-b border-slate-700 flex justify-between"><span>markdown</span><span>utf-8</span></div>
498
566
  <textarea readOnly className="w-full flex-1 bg-transparent text-slate-300 font-mono text-sm p-6 resize-none focus:outline-none leading-relaxed" value={selectedFile.content} spellCheck="false"></textarea>
499
567
  </div>
500
568
  )}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-feature",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "bin": {
5
5
  "spec-feature": "./bin/cli.js"
6
6
  },
@@ -20,6 +20,7 @@ Template helps form a task list for feature implementation in the **spec-feature
20
20
  - If a direction is not required, explicitly indicate under the corresponding subheading `Not required — reason: <explanation>`.
21
21
  - After filling the document, mark self-check checkboxes in the "Instruction execution control" section.
22
22
  - Do not start executing tasks until a separate user request explicitly references `spec/features/{FEATURE}/tasks.md`; ignore execution requests without that reference.
23
+ - **MANDATORY:** When executing tasks from `spec/features/{FEATURE}/tasks.md`, after completing all tasks (all checkboxes are marked as `[x]`), automatically execute `spec/core/verify.md` with the same **FEATURE** parameter to verify task completion and generate the verification report.
23
24
 
24
25
  **What needs to be revealed in tasks**
25
26
 
@@ -63,3 +64,5 @@ Template helps form a task list for feature implementation in the **spec-feature
63
64
  ```
64
65
 
65
66
  Write strictly in Markdown and automatically create/update the task list file. Do not proceed to task execution without a separate request. **Goal** — create/update file `/spec/features/{FEATURE}/tasks.md` with a list of WHAT we do and how we verify the result, based on **CONTEXT** and prepared materials.
67
+
68
+ **Important:** When a user requests to execute tasks from `spec/features/{FEATURE}/tasks.md`, after completing all tasks, automatically execute `spec/core/verify.md` with parameter `#{FEATURE}#` to verify completion and generate the verification report.