spec-feature 1.1.2 → 1.1.4

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.
Files changed (49) hide show
  1. package/bin/viewer.html +295 -88
  2. package/package.json +1 -1
  3. package/spec/core/tasks.md +3 -0
  4. package/spec/features/create-task/plan.md +36 -0
  5. package/spec/features/create-task/spec.md +33 -0
  6. package/spec/features/create-task/tasks.md +33 -0
  7. package/spec/features/create-task/verify-report.md +29 -0
  8. package/spec/features/image-zoom/plan.md +237 -0
  9. package/spec/features/image-zoom/spec.md +135 -0
  10. package/spec/features/image-zoom/tasks.md +105 -0
  11. package/spec/features/invite-user/plan.md +32 -0
  12. package/spec/features/invite-user/spec.md +31 -0
  13. package/spec/features/invite-user/tasks.md +31 -0
  14. package/spec/features/invite-user/verify-report.md +33 -0
  15. package/spec/features/list-projects/plan.md +40 -0
  16. package/spec/features/list-projects/spec.md +33 -0
  17. package/spec/features/list-projects/tasks.md +35 -0
  18. package/spec/features/list-tasks/plan.md +41 -0
  19. package/spec/features/list-tasks/spec.md +34 -0
  20. package/spec/features/list-tasks/tasks.md +41 -0
  21. package/spec/features/metrics/plan.md +41 -0
  22. package/spec/features/metrics/spec.md +37 -0
  23. package/spec/features/metrics/tasks.md +31 -0
  24. package/spec/features/project-edit/plan.md +36 -0
  25. package/spec/features/project-edit/spec.md +32 -0
  26. package/spec/features/project-edit/tasks.md +36 -0
  27. package/spec/features/review-order/plan.md +37 -0
  28. package/spec/features/review-order/spec.md +28 -0
  29. package/spec/features/review-order/tasks.md +26 -0
  30. package/spec/features/review-order/verify-report.md +19 -0
  31. package/spec/features/supabase-init/plan.md +89 -0
  32. package/spec/features/supabase-init/spec.md +73 -0
  33. package/spec/features/supabase-init/tasks.md +51 -0
  34. package/spec/features/task-details/plan.md +38 -0
  35. package/spec/features/task-details/spec.md +33 -0
  36. package/spec/features/task-details/tasks.md +33 -0
  37. package/spec/features/telegram-initData/plan.md +46 -0
  38. package/spec/features/telegram-initData/spec.md +40 -0
  39. package/spec/features/telegram-initData/tasks.md +52 -0
  40. package/spec/features/tg-user-save-to-supabase/plan.md +137 -0
  41. package/spec/features/tg-user-save-to-supabase/spec.md +54 -0
  42. package/spec/features/tg-user-save-to-supabase/tasks.md +58 -0
  43. package/spec/features/user-profile/plan.md +39 -0
  44. package/spec/features/user-profile/spec.md +36 -0
  45. package/spec/features/user-profile/tasks.md +38 -0
  46. package/spec/features/user-profile-edit/plan.md +36 -0
  47. package/spec/features/user-profile-edit/spec.md +33 -0
  48. package/spec/features/user-profile-edit/tasks.md +33 -0
  49. package/spec/features/user-profile-edit/verify-report.md +26 -0
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,153 @@
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 renderFormattedText = (text) => {
120
+ if (!text || typeof text !== 'string') return text;
121
+
122
+ const parts = [];
123
+ let keyCounter = 0;
124
+ let lastIndex = 0;
125
+
126
+ // Find all matches (code and bold) with their positions
127
+ const matches = [];
128
+
129
+ // Find inline code matches - matches `text` but not `` (empty backticks)
130
+ // Pattern: backtick, one or more non-backtick characters, backtick
131
+ const codeRegex = /`([^`]+)`/g;
132
+ let match;
133
+ while ((match = codeRegex.exec(text)) !== null) {
134
+ // Only add non-empty matches (at least one character between backticks)
135
+ if (match[1] && match[1].length > 0) {
136
+ matches.push({
137
+ start: match.index,
138
+ end: match.index + match[0].length,
139
+ content: match[1],
140
+ type: 'code'
141
+ });
142
+ }
143
+ }
144
+
145
+ // Find bold text matches - matches **text**
146
+ // Pattern: **, one or more non-asterisk characters (non-greedy), **
147
+ // This handles: **bold**, but not *** or ** (empty) or **bold*text** (nested asterisks)
148
+ const boldRegex = /\*\*([^*]+?)\*\*/g;
149
+ while ((match = boldRegex.exec(text)) !== null) {
150
+ // Only add non-empty matches (at least one character between **)
151
+ if (match[1] && match[1].length > 0) {
152
+ matches.push({
153
+ start: match.index,
154
+ end: match.index + match[0].length,
155
+ content: match[1],
156
+ type: 'bold'
157
+ });
158
+ }
159
+ }
160
+
161
+ // If no matches found, return original text
162
+ if (matches.length === 0) {
163
+ return text;
164
+ }
165
+
166
+ // Sort matches by position
167
+ matches.sort((a, b) => a.start - b.start);
168
+
169
+ // Remove overlapping matches (prioritize code over bold)
170
+ // First pass: mark overlapping matches for removal
171
+ const toRemove = new Set();
172
+ for (let i = 0; i < matches.length; i++) {
173
+ if (toRemove.has(i)) continue;
174
+ const current = matches[i];
175
+ for (let j = i + 1; j < matches.length; j++) {
176
+ if (toRemove.has(j)) continue;
177
+ const other = matches[j];
178
+ // Check if matches overlap
179
+ if (current.start < other.end && current.end > other.start) {
180
+ // Prioritize code over bold
181
+ if (current.type === 'code' && other.type === 'bold') {
182
+ toRemove.add(j);
183
+ } else if (current.type === 'bold' && other.type === 'code') {
184
+ toRemove.add(i);
185
+ break; // Current is removed, no need to check further
186
+ } else {
187
+ // Same type overlapping - keep the first one
188
+ toRemove.add(j);
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ // Filter out removed matches
195
+ const filteredMatches = matches.filter((_, index) => !toRemove.has(index));
196
+
197
+ // Build result array
198
+ filteredMatches.forEach(match => {
199
+ // Add text before match (handles case when match is at start of string)
200
+ if (match.start > lastIndex) {
201
+ const beforeText = text.substring(lastIndex, match.start);
202
+ if (beforeText) {
203
+ parts.push(beforeText);
204
+ }
205
+ }
206
+
207
+ // Add formatted match (always add, even if content is empty to preserve structure)
208
+ if (match.type === 'code') {
209
+ parts.push(
210
+ <code key={keyCounter++} className="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-700 text-slate-800 dark:text-slate-200 rounded text-sm font-mono">
211
+ {match.content}
212
+ </code>
213
+ );
214
+ } else if (match.type === 'bold') {
215
+ parts.push(
216
+ <strong key={keyCounter++} className="font-semibold text-slate-900 dark:text-slate-100">
217
+ {match.content}
218
+ </strong>
219
+ );
220
+ }
221
+
222
+ lastIndex = match.end;
223
+ });
224
+
225
+ // Add remaining text (handles case when match is at end of string)
226
+ if (lastIndex < text.length) {
227
+ const remainingText = text.substring(lastIndex);
228
+ if (remainingText) {
229
+ parts.push(remainingText);
230
+ }
231
+ }
232
+
233
+ // Return formatted parts or original text if something went wrong
234
+ return parts.length > 0 ? parts : text;
235
+ };
236
+
237
+ const renderBoldText = (text) => {
238
+ const parts = text.split('**');
239
+ return parts.map((part, idx) =>
240
+ idx % 2 === 1 ? <strong key={idx} className="font-semibold text-slate-900 dark:text-slate-100">{part}</strong> : part
241
+ );
242
+ };
243
+
108
244
  const LinkRenderer = ({ text, onNavigate }) => {
109
245
  const fileRegex = /(spec\.md|plan\.md|tasks\.md|verify-report\.md)/g;
110
246
  const parts = text.split(fileRegex);
111
- if (parts.length === 1) return <span className="text-slate-600">{text}</span>;
247
+ if (parts.length === 1) {
248
+ return <span className="text-slate-600 dark:text-slate-400">{renderFormattedText(text)}</span>;
249
+ }
112
250
  return (
113
- <span className="text-slate-600">
251
+ <span className="text-slate-600 dark:text-slate-400">
114
252
  {parts.map((part, i) => {
115
253
  if (part.match(fileRegex)) {
116
254
  let targetId = 'spec';
@@ -118,12 +256,12 @@
118
256
  if (part.includes('tasks')) targetId = 'tasks';
119
257
  if (part.includes('verify')) targetId = 'verify';
120
258
  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}`}>
259
+ <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
260
  {part} <ExternalLink size={10} />
123
261
  </button>
124
262
  );
125
263
  }
126
- return part;
264
+ return renderFormattedText(part);
127
265
  })}
128
266
  </span>
129
267
  );
@@ -143,7 +281,7 @@
143
281
  if (codeBlockLang === 'mermaid') {
144
282
  renderLines.push(<MermaidBlock key={`mermaid-${i}`} content={codeBlockContent.join('\n')} />);
145
283
  } 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>);
284
+ 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
285
  }
148
286
  inCodeBlock = false; codeBlockContent = []; codeBlockLang = '';
149
287
  } else {
@@ -152,25 +290,51 @@
152
290
  continue;
153
291
  }
154
292
  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>);
293
+ if (line.startsWith('# ')) {
294
+ const h1Text = line.replace('# ', '');
295
+ 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">{renderFormattedText(h1Text)}</h1>);
296
+ }
297
+ else if (line.startsWith('## ')) {
298
+ const h2Text = line.replace('## ', '');
299
+ 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">{renderFormattedText(h2Text)}</h2>);
300
+ }
301
+ else if (line.startsWith('### ')) {
302
+ const h3Text = line.replace('### ', '');
303
+ renderLines.push(<h3 key={i} className="text-md font-semibold text-slate-700 dark:text-slate-300 mt-6 mb-2">{renderFormattedText(h3Text)}</h3>);
304
+ }
158
305
  else if (line.trim().startsWith('- [ ]') || line.trim().startsWith('- [x]')) {
159
306
  const isChecked = line.trim().startsWith('- [x]');
160
307
  const text = line.replace(/- \[[x ]\] /, '');
161
308
  renderLines.push(
162
309
  <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'}`}>
310
+ <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
311
  {isChecked && <Check size={12} strokeWidth={3} />}
165
312
  </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)}
313
+ <span className={`${isChecked ? 'text-slate-400 dark:text-slate-500 line-through' : 'text-slate-700 dark:text-slate-300'}`}>
314
+ {renderFormattedText(text)}
168
315
  </span>
169
316
  </div>
170
317
  );
171
318
  }
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>);
319
+ else if (line.startsWith('- ')) {
320
+ const listText = line.replace('- ', '');
321
+ renderLines.push(
322
+ <li key={i} className="ml-5 list-disc text-slate-700 dark:text-slate-300 pl-1">
323
+ {renderFormattedText(listText)}
324
+ </li>
325
+ );
326
+ }
327
+ else if (/^\d+\. /.test(line)) {
328
+ const numMatch = line.match(/^(\d+)\. (.*)/);
329
+ if (numMatch) {
330
+ renderLines.push(
331
+ <div key={i} className="ml-5 text-slate-700 dark:text-slate-300 flex gap-2">
332
+ <span className="font-mono text-slate-400 dark:text-slate-500 font-bold w-4 text-right">{numMatch[1]}.</span>
333
+ <span>{renderFormattedText(numMatch[2])}</span>
334
+ </div>
335
+ );
336
+ }
337
+ }
174
338
  else if (line.trim() === '') renderLines.push(<div key={i} className="h-2"></div>);
175
339
  else renderLines.push(<p key={i} className="leading-relaxed"><LinkRenderer text={line} onNavigate={onNavigate} /></p>);
176
340
  }
@@ -188,16 +352,16 @@
188
352
  };
189
353
 
190
354
  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'}`}>
355
+ <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'}`}>
356
+ <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
357
  <FileIcon type={file.type} />
194
358
  </div>
195
359
  <div className="flex flex-col overflow-hidden w-full">
196
360
  <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" />}
361
+ <span className={`font-medium truncate ${isActive ? 'text-slate-900 dark:text-slate-100' : 'text-slate-600 dark:text-slate-400'}`}>{file.name}</span>
362
+ {isActive && <ChevronRight size={14} className="text-blue-500 dark:text-blue-400 shrink-0" />}
199
363
  </div>
200
- <span className="text-[10px] text-slate-400 uppercase tracking-wider font-semibold truncate">{file.description}</span>
364
+ <span className="text-[10px] text-slate-400 dark:text-slate-500 uppercase tracking-wider font-semibold truncate">{file.description}</span>
201
365
  </div>
202
366
  </button>
203
367
  );
@@ -207,75 +371,96 @@
207
371
  return (
208
372
  <div className="w-full">
209
373
  <div className="flex justify-between text-xs mb-1.5">
210
- <span className={`font-semibold ${percentage === 100 ? 'text-green-600' : 'text-slate-600'}`}>
374
+ <span className={`font-semibold ${percentage === 100 ? 'text-green-600 dark:text-green-400' : 'text-slate-600 dark:text-slate-400'}`}>
211
375
  {percentage === 100 ? 'Done' : `${percentage}%`}
212
376
  </span>
213
- <span className="text-slate-400">{completed}/{total}</span>
377
+ <span className="text-slate-400 dark:text-slate-500">{completed}/{total}</span>
214
378
  </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>
379
+ <div className="h-1.5 w-full bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
380
+ <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
381
  </div>
218
382
  </div>
219
383
  );
220
384
  }
221
385
 
222
386
  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">
387
+ <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
388
  <div className="flex justify-between items-end mb-2">
225
389
  <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">
390
+ <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>
391
+ <div className="flex items-center gap-2 text-sm font-bold text-slate-800 dark:text-slate-200">
228
392
  {percentage === 100 ? (
229
- <span className="flex items-center gap-1.5 text-green-600"><Check size={14} strokeWidth={3}/> Ready to Verify</span>
393
+ <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
394
  ) : (
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>
395
+ <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
396
  )}
233
397
  </div>
234
398
  </div>
235
- <div className="text-xs font-mono font-medium text-slate-400 group-hover:text-slate-600">{completed}/{total}</div>
399
+ <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
400
  </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}%` }} />
401
+ <div className="h-1.5 w-full bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
402
+ <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
403
  </div>
240
404
  </button>
241
405
  );
242
406
  };
243
407
 
408
+ const PreviewText = ({ content, maxLength = 150 }) => {
409
+ if (!content) return <span>No description</span>;
410
+
411
+ // Extract first paragraph, remove headers and code blocks
412
+ const lines = content.split('\n');
413
+ let previewText = '';
414
+ for (let i = 0; i < lines.length && previewText.length < maxLength; i++) {
415
+ const line = lines[i].trim();
416
+ if (line && !line.startsWith('#') && !line.startsWith('```') && !line.startsWith('- [') && !line.startsWith('- ')) {
417
+ previewText += (previewText ? ' ' : '') + line;
418
+ }
419
+ }
420
+
421
+ if (previewText.length > maxLength) {
422
+ previewText = previewText.substring(0, maxLength) + '...';
423
+ }
424
+
425
+ return <span>{renderFormattedText(previewText || 'No description')}</span>;
426
+ };
427
+
244
428
  const Dashboard = ({ features, onSelectFeature }) => {
245
429
  return (
246
430
  <div className="p-8 max-w-6xl mx-auto">
247
431
  <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>
432
+ <div className="p-2 bg-indigo-600 dark:bg-indigo-500 rounded-lg text-white"><FolderGit size={24} /></div>
249
433
  <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>
434
+ <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Project Specifications</h1>
435
+ <p className="text-slate-500 dark:text-slate-400 text-sm">Overview of all active features and their implementation status</p>
252
436
  </div>
253
437
  </div>
254
438
 
255
439
  {features.length === 0 ? (
256
- <div className="text-slate-500 text-sm">No features found.</div>
440
+ <div className="text-slate-500 dark:text-slate-400 text-sm">No features found.</div>
257
441
  ) : (
258
442
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
259
443
  {features.map(feature => {
260
444
  const progress = calculateProgress(feature.files);
445
+ const specFile = feature.files.find(f => f.type === 'spec');
261
446
  return (
262
447
  <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">
448
+ 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
449
  <div className="flex items-start justify-between mb-4">
265
450
  <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>
451
+ <span className="text-slate-400 dark:text-slate-500 text-sm font-mono">#</span>
452
+ <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
453
  </div>
269
- {progress.percentage === 100 && <div className="text-green-500"><ShieldCheck size={18} /></div>}
454
+ {progress.percentage === 100 && <div className="text-green-500 dark:text-green-400"><ShieldCheck size={18} /></div>}
270
455
  </div>
271
456
 
272
457
  <div className="flex-1">
273
- <p className="text-xs text-slate-500 mb-4 line-clamp-2">
274
- {feature.files.find(f => f.type === 'spec')?.content.slice(0, 100).replace(/#/g, '') || 'No description'}...
458
+ <p className="text-xs text-slate-500 dark:text-slate-400 mb-4 line-clamp-2">
459
+ <PreviewText content={specFile?.content} maxLength={150} />
275
460
  </p>
276
461
  </div>
277
462
 
278
- <div className="mt-auto pt-4 border-t border-slate-100">
463
+ <div className="mt-auto pt-4 border-t border-slate-100 dark:border-slate-700">
279
464
  <ProgressBar
280
465
  minimal
281
466
  percentage={progress.percentage}
@@ -303,8 +488,22 @@
303
488
  const [selectedFileId, setSelectedFileId] = useState('spec');
304
489
  const [isSidebarOpen, setSidebarOpen] = useState(true);
305
490
  const [viewMode, setViewMode] = useState('preview');
491
+ const [isDark, setIsDark] = useState(() => {
492
+ const saved = localStorage.getItem('theme');
493
+ if (saved) return saved === 'dark';
494
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
495
+ });
306
496
  const initializedWithoutFeatures = CONFIG.isInitialized && !CONFIG.hasFeatures;
307
497
 
498
+ useEffect(() => {
499
+ if (isDark) {
500
+ document.documentElement.classList.add('dark');
501
+ } else {
502
+ document.documentElement.classList.remove('dark');
503
+ }
504
+ localStorage.setItem('theme', isDark ? 'dark' : 'light');
505
+ }, [isDark]);
506
+
308
507
  useEffect(() => {
309
508
  fetchFeatures()
310
509
  .then(data => {
@@ -358,39 +557,44 @@
358
557
  // --- VIEW: DASHBOARD ---
359
558
  if (!activeFeatureId) {
360
559
  return (
361
- <div className="min-h-screen bg-slate-50 font-sans text-slate-900 overflow-auto">
560
+ <div className="min-h-screen bg-slate-50 dark:bg-slate-900 font-sans text-slate-900 dark:text-slate-100 overflow-auto">
561
+ <div className="fixed top-4 right-4 z-50">
562
+ <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'}>
563
+ {isDark ? <Sun size={20} /> : <Moon size={20} />}
564
+ </button>
565
+ </div>
362
566
  {loading ? (
363
- <div className="p-8 max-w-6xl mx-auto text-slate-500">Loading features...</div>
567
+ <div className="p-8 max-w-6xl mx-auto text-slate-500 dark:text-slate-400">Loading features...</div>
364
568
  ) : error ? (
365
569
  <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">
570
+ <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
571
  {error}
368
572
  </div>
369
573
  </div>
370
574
  ) : features.length === 0 ? (
371
575
  <div className="p-8 max-w-5xl mx-auto space-y-6">
372
576
  <div className="flex items-center gap-3">
373
- <div className="p-2 bg-indigo-600 rounded-lg text-white"><FolderGit size={24} /></div>
577
+ <div className="p-2 bg-indigo-600 dark:bg-indigo-500 rounded-lg text-white"><FolderGit size={24} /></div>
374
578
  <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>
579
+ <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Project Specifications</h1>
580
+ <p className="text-slate-500 dark:text-slate-400 text-sm">Overview of all active features and their implementation status</p>
377
581
  </div>
378
582
  </div>
379
583
  {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>
584
+ <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">
585
+ <div className="font-semibold text-slate-900 dark:text-slate-100">No features found. Create your first specification:</div>
382
586
  <div className="space-y-2">
383
587
  <div className="text-sm">1. Open your AI assistant (Claude, Cursor, Copilot, etc.)</div>
384
588
  <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.
589
+ <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
590
  #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>
591
+ <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>
592
+ <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>
593
+ <div className="text-sm font-medium text-slate-700 dark:text-slate-300">4. Refresh this page to see your new feature</div>
390
594
  </div>
391
595
  </div>
392
596
  ) : (
393
- <div className="bg-white border border-slate-200 rounded-xl p-6 text-slate-600 shadow-sm">
597
+ <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
598
  No features found in folder "{CONFIG.folderName}". Use --folder to point to a valid specs directory.
395
599
  </div>
396
600
  )}
@@ -405,11 +609,11 @@
405
609
  // --- VIEW: FEATURE DETAILS (Legacy View) ---
406
610
  if (!activeFeature) {
407
611
  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">
612
+ <div className="min-h-screen bg-slate-50 dark:bg-slate-900 p-10 text-slate-600 dark:text-slate-400">
613
+ <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
614
  Feature not found. Please go back to dashboard.
411
615
  <div className="mt-4">
412
- <button onClick={handleBackToDashboard} className="px-4 py-2 bg-blue-600 text-white rounded-md text-sm">Back</button>
616
+ <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
617
  </div>
414
618
  </div>
415
619
  </div>
@@ -417,35 +621,35 @@
417
621
  }
418
622
 
419
623
  return (
420
- <div className="flex h-screen bg-slate-50 overflow-hidden font-sans text-slate-900">
624
+ <div className="flex h-screen bg-slate-50 dark:bg-slate-900 overflow-hidden font-sans text-slate-900 dark:text-slate-100">
421
625
 
422
626
  {/* Mobile Sidebar Overlay */}
423
627
  {!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">
628
+ <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
629
  <MenuIcon size={20} />
426
630
  </button>
427
631
  )}
428
632
 
429
633
  {/* 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'}`}>
634
+ <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
635
  <div className="flex flex-col h-full">
432
636
 
433
637
  {/* 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">
638
+ <div className="h-16 flex items-center px-4 border-b border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800">
639
+ <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">
640
+ <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
641
  <ChevronLeft size={16} />
438
642
  </div>
439
643
  Back to Features
440
644
  </button>
441
- <button onClick={() => setSidebarOpen(false)} className="lg:hidden ml-auto p-1 text-slate-400 hover:text-slate-600"><X size={20} /></button>
645
+ <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
646
  </div>
443
647
 
444
648
  {/* 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>
649
+ <div className="px-5 py-4 bg-slate-50/50 dark:bg-slate-900/50">
650
+ <div className="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider mb-1.5">Active Feature</div>
651
+ <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">
652
+ <span className="text-blue-500 dark:text-blue-400">#</span> <span className="truncate">{activeFeature.title}</span>
449
653
  </div>
450
654
  </div>
451
655
 
@@ -459,42 +663,45 @@
459
663
  </div>
460
664
 
461
665
  {/* Footer Progress */}
462
- <div className="p-4 border-t border-slate-200 bg-white">
666
+ <div className="p-4 border-t border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800">
463
667
  <ProgressBar percentage={stats.percentage} total={stats.total} completed={stats.completed} onClick={() => handleNavigateFile('tasks')} />
464
668
  </div>
465
669
  </div>
466
670
  </aside>
467
671
 
468
672
  {/* Main Content */}
469
- <main className="flex-1 flex flex-col h-full min-w-0 bg-white relative">
673
+ <main className="flex-1 flex flex-col h-full min-w-0 bg-white dark:bg-slate-900 relative">
470
674
  {/* Header */}
471
- <header className="h-16 flex items-center justify-between px-6 border-b border-slate-100 bg-white shrink-0 z-10">
675
+ <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
676
  <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>
677
+ <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>
678
+ <h1 className="text-lg font-bold text-slate-800 dark:text-slate-200 truncate">{selectedFile.name}</h1>
475
679
  </div>
476
680
  <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>
681
+ <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'}>
682
+ {isDark ? <Sun size={18} /> : <Moon size={18} />}
683
+ </button>
684
+ <div className="flex bg-slate-100/80 dark:bg-slate-800 p-1 rounded-lg border border-slate-200 dark:border-slate-700">
685
+ <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>
686
+ <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
687
  </div>
481
- <div className="w-px h-6 bg-slate-200 mx-1"></div>
688
+ <div className="w-px h-6 bg-slate-200 dark:bg-slate-700 mx-1"></div>
482
689
  {/* Copy Button Logic embedded here for brevity */}
483
690
  <button onClick={() => {
484
691
  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>
692
+ }} 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
693
  </div>
487
694
  </header>
488
695
 
489
696
  {/* Content */}
490
- <div className="flex-1 overflow-auto bg-slate-50/30 scroll-smooth">
697
+ <div className="flex-1 overflow-auto bg-slate-50/30 dark:bg-slate-900 scroll-smooth">
491
698
  <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]">
699
+ <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
700
  {viewMode === 'preview' ? (
494
701
  <div className="p-10 lg:p-14"><SmartMarkdownRenderer content={selectedFile.content} onNavigate={handleNavigateFile} /></div>
495
702
  ) : (
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>
703
+ <div className="bg-[#1e293b] dark:bg-[#0f172a] h-full min-h-[70vh] flex flex-col">
704
+ <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
705
  <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
706
  </div>
500
707
  )}