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.
- package/bin/viewer.html +295 -88
- package/package.json +1 -1
- package/spec/core/tasks.md +3 -0
- package/spec/features/create-task/plan.md +36 -0
- package/spec/features/create-task/spec.md +33 -0
- package/spec/features/create-task/tasks.md +33 -0
- package/spec/features/create-task/verify-report.md +29 -0
- package/spec/features/image-zoom/plan.md +237 -0
- package/spec/features/image-zoom/spec.md +135 -0
- package/spec/features/image-zoom/tasks.md +105 -0
- package/spec/features/invite-user/plan.md +32 -0
- package/spec/features/invite-user/spec.md +31 -0
- package/spec/features/invite-user/tasks.md +31 -0
- package/spec/features/invite-user/verify-report.md +33 -0
- package/spec/features/list-projects/plan.md +40 -0
- package/spec/features/list-projects/spec.md +33 -0
- package/spec/features/list-projects/tasks.md +35 -0
- package/spec/features/list-tasks/plan.md +41 -0
- package/spec/features/list-tasks/spec.md +34 -0
- package/spec/features/list-tasks/tasks.md +41 -0
- package/spec/features/metrics/plan.md +41 -0
- package/spec/features/metrics/spec.md +37 -0
- package/spec/features/metrics/tasks.md +31 -0
- package/spec/features/project-edit/plan.md +36 -0
- package/spec/features/project-edit/spec.md +32 -0
- package/spec/features/project-edit/tasks.md +36 -0
- package/spec/features/review-order/plan.md +37 -0
- package/spec/features/review-order/spec.md +28 -0
- package/spec/features/review-order/tasks.md +26 -0
- package/spec/features/review-order/verify-report.md +19 -0
- package/spec/features/supabase-init/plan.md +89 -0
- package/spec/features/supabase-init/spec.md +73 -0
- package/spec/features/supabase-init/tasks.md +51 -0
- package/spec/features/task-details/plan.md +38 -0
- package/spec/features/task-details/spec.md +33 -0
- package/spec/features/task-details/tasks.md +33 -0
- package/spec/features/telegram-initData/plan.md +46 -0
- package/spec/features/telegram-initData/spec.md +40 -0
- package/spec/features/telegram-initData/tasks.md +52 -0
- package/spec/features/tg-user-save-to-supabase/plan.md +137 -0
- package/spec/features/tg-user-save-to-supabase/spec.md +54 -0
- package/spec/features/tg-user-save-to-supabase/tasks.md +58 -0
- package/spec/features/user-profile/plan.md +39 -0
- package/spec/features/user-profile/spec.md +36 -0
- package/spec/features/user-profile/tasks.md +38 -0
- package/spec/features/user-profile-edit/plan.md +36 -0
- package/spec/features/user-profile-edit/spec.md +33 -0
- package/spec/features/user-profile-edit/tasks.md +33 -0
- 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)
|
|
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('# '))
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
{
|
|
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('- '))
|
|
173
|
-
|
|
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
|
-
{
|
|
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/<feature-name>/</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/<feature-name>/</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
|
-
<
|
|
478
|
-
|
|
479
|
-
|
|
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
|
)}
|