@locusai/web 0.1.0
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/LICENSE +21 -0
- package/next.config.js +7 -0
- package/package.json +37 -0
- package/postcss.config.mjs +5 -0
- package/src/app/backlog/page.tsx +19 -0
- package/src/app/docs/page.tsx +7 -0
- package/src/app/globals.css +603 -0
- package/src/app/layout.tsx +43 -0
- package/src/app/page.tsx +16 -0
- package/src/app/providers.tsx +16 -0
- package/src/app/settings/page.tsx +194 -0
- package/src/components/BoardFilter.tsx +98 -0
- package/src/components/Header.tsx +21 -0
- package/src/components/PropertyItem.tsx +98 -0
- package/src/components/Sidebar.tsx +109 -0
- package/src/components/TaskCard.tsx +138 -0
- package/src/components/TaskCreateModal.tsx +243 -0
- package/src/components/TaskPanel.tsx +765 -0
- package/src/components/index.ts +7 -0
- package/src/components/ui/Badge.tsx +77 -0
- package/src/components/ui/Button.tsx +47 -0
- package/src/components/ui/Checkbox.tsx +52 -0
- package/src/components/ui/Dropdown.tsx +107 -0
- package/src/components/ui/Input.tsx +36 -0
- package/src/components/ui/Modal.tsx +79 -0
- package/src/components/ui/Textarea.tsx +21 -0
- package/src/components/ui/index.ts +7 -0
- package/src/hooks/useTasks.ts +119 -0
- package/src/lib/api-client.ts +24 -0
- package/src/lib/utils.ts +6 -0
- package/src/services/doc.service.ts +27 -0
- package/src/services/index.ts +3 -0
- package/src/services/sprint.service.ts +26 -0
- package/src/services/task.service.ts +75 -0
- package/src/views/Backlog.tsx +691 -0
- package/src/views/Board.tsx +306 -0
- package/src/views/Docs.tsx +625 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import MDEditor from "@uiw/react-md-editor";
|
|
4
|
+
import {
|
|
5
|
+
BookOpen,
|
|
6
|
+
Code,
|
|
7
|
+
File,
|
|
8
|
+
FileCode,
|
|
9
|
+
FileText,
|
|
10
|
+
Filter,
|
|
11
|
+
Folder,
|
|
12
|
+
FolderOpen,
|
|
13
|
+
Lightbulb,
|
|
14
|
+
Palette,
|
|
15
|
+
Plus,
|
|
16
|
+
Save,
|
|
17
|
+
Search,
|
|
18
|
+
Users,
|
|
19
|
+
X,
|
|
20
|
+
} from "lucide-react";
|
|
21
|
+
import { useCallback, useEffect, useState } from "react";
|
|
22
|
+
import { Button, Input } from "@/components/ui";
|
|
23
|
+
import { cn } from "@/lib/utils";
|
|
24
|
+
import { DocNode, docService } from "@/services";
|
|
25
|
+
|
|
26
|
+
// Document categories for role-based filtering
|
|
27
|
+
const DOC_CATEGORIES = [
|
|
28
|
+
{
|
|
29
|
+
id: "all",
|
|
30
|
+
label: "All Documents",
|
|
31
|
+
icon: BookOpen,
|
|
32
|
+
color: "text-foreground",
|
|
33
|
+
bgColor: "bg-secondary",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "product",
|
|
37
|
+
label: "Product",
|
|
38
|
+
icon: Lightbulb,
|
|
39
|
+
color: "text-amber-500",
|
|
40
|
+
bgColor: "bg-amber-500/10",
|
|
41
|
+
description: "PRDs, roadmaps, specs",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "engineering",
|
|
45
|
+
label: "Engineering",
|
|
46
|
+
icon: Code,
|
|
47
|
+
color: "text-blue-500",
|
|
48
|
+
bgColor: "bg-blue-500/10",
|
|
49
|
+
description: "Technical docs, APIs",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "design",
|
|
53
|
+
label: "Design",
|
|
54
|
+
icon: Palette,
|
|
55
|
+
color: "text-purple-500",
|
|
56
|
+
bgColor: "bg-purple-500/10",
|
|
57
|
+
description: "UI specs, guidelines",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "team",
|
|
61
|
+
label: "Team",
|
|
62
|
+
icon: Users,
|
|
63
|
+
color: "text-emerald-500",
|
|
64
|
+
bgColor: "bg-emerald-500/10",
|
|
65
|
+
description: "Processes, onboarding",
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// Template options for new documents
|
|
70
|
+
const DOC_TEMPLATES = [
|
|
71
|
+
{ id: "blank", label: "Blank Document", content: "# Untitled\n\n" },
|
|
72
|
+
{
|
|
73
|
+
id: "prd",
|
|
74
|
+
label: "Product Spec (PRD)",
|
|
75
|
+
category: "product",
|
|
76
|
+
content: `# Product Requirements Document
|
|
77
|
+
|
|
78
|
+
## Overview
|
|
79
|
+
Brief description of the feature/product.
|
|
80
|
+
|
|
81
|
+
## Goals
|
|
82
|
+
- Goal 1
|
|
83
|
+
- Goal 2
|
|
84
|
+
|
|
85
|
+
## User Stories
|
|
86
|
+
As a [user type], I want [action] so that [benefit].
|
|
87
|
+
|
|
88
|
+
## Requirements
|
|
89
|
+
### Functional Requirements
|
|
90
|
+
1.
|
|
91
|
+
|
|
92
|
+
### Non-Functional Requirements
|
|
93
|
+
1.
|
|
94
|
+
|
|
95
|
+
## Success Metrics
|
|
96
|
+
-
|
|
97
|
+
|
|
98
|
+
## Timeline
|
|
99
|
+
| Phase | Description | Date |
|
|
100
|
+
|-------|-------------|------|
|
|
101
|
+
| | | |
|
|
102
|
+
`,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "technical",
|
|
106
|
+
label: "Technical Design",
|
|
107
|
+
category: "engineering",
|
|
108
|
+
content: `# Technical Design Document
|
|
109
|
+
|
|
110
|
+
## Summary
|
|
111
|
+
Brief technical overview.
|
|
112
|
+
|
|
113
|
+
## Architecture
|
|
114
|
+
Describe the system architecture.
|
|
115
|
+
|
|
116
|
+
## API Design
|
|
117
|
+
\`\`\`typescript
|
|
118
|
+
// API endpoints
|
|
119
|
+
\`\`\`
|
|
120
|
+
|
|
121
|
+
## Database Schema
|
|
122
|
+
\`\`\`sql
|
|
123
|
+
-- Schema changes
|
|
124
|
+
\`\`\`
|
|
125
|
+
|
|
126
|
+
## Implementation Plan
|
|
127
|
+
1.
|
|
128
|
+
|
|
129
|
+
## Testing Strategy
|
|
130
|
+
- Unit tests
|
|
131
|
+
- Integration tests
|
|
132
|
+
|
|
133
|
+
## Rollout Plan
|
|
134
|
+
-
|
|
135
|
+
`,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "api",
|
|
139
|
+
label: "API Documentation",
|
|
140
|
+
category: "engineering",
|
|
141
|
+
content: `# API Documentation
|
|
142
|
+
|
|
143
|
+
## Endpoints
|
|
144
|
+
|
|
145
|
+
### GET /api/resource
|
|
146
|
+
Description of the endpoint.
|
|
147
|
+
|
|
148
|
+
**Parameters:**
|
|
149
|
+
| Name | Type | Required | Description |
|
|
150
|
+
|------|------|----------|-------------|
|
|
151
|
+
| | | | |
|
|
152
|
+
|
|
153
|
+
**Response:**
|
|
154
|
+
\`\`\`json
|
|
155
|
+
{
|
|
156
|
+
"data": []
|
|
157
|
+
}
|
|
158
|
+
\`\`\`
|
|
159
|
+
|
|
160
|
+
### POST /api/resource
|
|
161
|
+
`,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "runbook",
|
|
165
|
+
label: "Runbook",
|
|
166
|
+
category: "engineering",
|
|
167
|
+
content: `# Runbook: [Service Name]
|
|
168
|
+
|
|
169
|
+
## Overview
|
|
170
|
+
What this service does.
|
|
171
|
+
|
|
172
|
+
## Common Issues
|
|
173
|
+
|
|
174
|
+
### Issue 1
|
|
175
|
+
**Symptoms:**
|
|
176
|
+
**Resolution:**
|
|
177
|
+
|
|
178
|
+
## Monitoring
|
|
179
|
+
- Dashboard:
|
|
180
|
+
- Alerts:
|
|
181
|
+
|
|
182
|
+
## Contacts
|
|
183
|
+
- Team:
|
|
184
|
+
- Escalation:
|
|
185
|
+
`,
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
export function Docs() {
|
|
190
|
+
const [tree, setTree] = useState<DocNode[]>([]);
|
|
191
|
+
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
|
192
|
+
const [content, setContent] = useState("");
|
|
193
|
+
const [originalContent, setOriginalContent] = useState("");
|
|
194
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
195
|
+
const [newFileName, setNewFileName] = useState("");
|
|
196
|
+
const [selectedTemplate, setSelectedTemplate] = useState("blank");
|
|
197
|
+
const [contentMode, setContentMode] = useState<"edit" | "preview">("edit");
|
|
198
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
199
|
+
const [activeCategory, setActiveCategory] = useState("all");
|
|
200
|
+
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
|
|
201
|
+
new Set()
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const hasUnsavedChanges = content !== originalContent;
|
|
205
|
+
|
|
206
|
+
const fetchTree = useCallback(async () => {
|
|
207
|
+
try {
|
|
208
|
+
const data = await docService.getTree();
|
|
209
|
+
setTree(data);
|
|
210
|
+
// Auto-expand all folders
|
|
211
|
+
const folders = new Set<string>();
|
|
212
|
+
const collectFolders = (nodes: DocNode[]) => {
|
|
213
|
+
nodes.forEach((n) => {
|
|
214
|
+
if (n.type === "directory") {
|
|
215
|
+
folders.add(n.path);
|
|
216
|
+
if (n.children) collectFolders(n.children);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
collectFolders(data);
|
|
221
|
+
setExpandedFolders(folders);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error("Failed to fetch doc tree", err);
|
|
224
|
+
}
|
|
225
|
+
}, []);
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
fetchTree();
|
|
229
|
+
}, [fetchTree]);
|
|
230
|
+
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
if (selectedPath) {
|
|
233
|
+
docService.read(selectedPath).then((data) => {
|
|
234
|
+
setContent(data.content || "");
|
|
235
|
+
setOriginalContent(data.content || "");
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}, [selectedPath]);
|
|
239
|
+
|
|
240
|
+
const handleSave = async () => {
|
|
241
|
+
if (!selectedPath) return;
|
|
242
|
+
try {
|
|
243
|
+
await docService.write(selectedPath, content);
|
|
244
|
+
setOriginalContent(content);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.error("Failed to save document", err);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const handleCreateFile = async () => {
|
|
251
|
+
if (!newFileName.trim()) return;
|
|
252
|
+
|
|
253
|
+
// Build path with category prefix
|
|
254
|
+
let path = newFileName;
|
|
255
|
+
if (activeCategory !== "all" && !newFileName.includes("/")) {
|
|
256
|
+
path = `${activeCategory}/${newFileName}`;
|
|
257
|
+
}
|
|
258
|
+
if (!path.endsWith(".md")) path += ".md";
|
|
259
|
+
|
|
260
|
+
const template = DOC_TEMPLATES.find((t) => t.id === selectedTemplate);
|
|
261
|
+
const initialContent =
|
|
262
|
+
template?.content ||
|
|
263
|
+
`# ${newFileName.split("/").pop()?.replace(".md", "") || "New Document"}\n\n`;
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
await docService.write(path, initialContent);
|
|
267
|
+
setIsCreating(false);
|
|
268
|
+
setNewFileName("");
|
|
269
|
+
setSelectedTemplate("blank");
|
|
270
|
+
fetchTree();
|
|
271
|
+
setSelectedPath(path);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.error("Failed to create file", err);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const toggleFolder = (path: string) => {
|
|
278
|
+
setExpandedFolders((prev) => {
|
|
279
|
+
const next = new Set(prev);
|
|
280
|
+
if (next.has(path)) {
|
|
281
|
+
next.delete(path);
|
|
282
|
+
} else {
|
|
283
|
+
next.add(path);
|
|
284
|
+
}
|
|
285
|
+
return next;
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Filter documents based on search and category
|
|
290
|
+
const filterNodes = (nodes: DocNode[]): DocNode[] => {
|
|
291
|
+
return nodes
|
|
292
|
+
.map((node) => {
|
|
293
|
+
if (node.type === "directory") {
|
|
294
|
+
const filteredChildren = node.children
|
|
295
|
+
? filterNodes(node.children)
|
|
296
|
+
: [];
|
|
297
|
+
// Keep directory if it has matching children or matches search
|
|
298
|
+
if (
|
|
299
|
+
filteredChildren.length > 0 ||
|
|
300
|
+
node.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
301
|
+
) {
|
|
302
|
+
return { ...node, children: filteredChildren };
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
} else {
|
|
306
|
+
// File node - check category and search
|
|
307
|
+
const matchesCategory =
|
|
308
|
+
activeCategory === "all" ||
|
|
309
|
+
node.path.toLowerCase().startsWith(activeCategory);
|
|
310
|
+
const matchesSearch = node.name
|
|
311
|
+
.toLowerCase()
|
|
312
|
+
.includes(searchQuery.toLowerCase());
|
|
313
|
+
if (matchesCategory && matchesSearch) return node;
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
.filter(Boolean) as DocNode[];
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const filteredTree = filterNodes(tree);
|
|
321
|
+
|
|
322
|
+
const renderTree = (nodes: DocNode[], depth = 0) => {
|
|
323
|
+
return nodes.map((node) => {
|
|
324
|
+
const isExpanded = expandedFolders.has(node.path);
|
|
325
|
+
const isSelected = selectedPath === node.path;
|
|
326
|
+
|
|
327
|
+
if (node.type === "directory") {
|
|
328
|
+
return (
|
|
329
|
+
<div key={node.path}>
|
|
330
|
+
<button
|
|
331
|
+
className={cn(
|
|
332
|
+
"flex items-center gap-2 w-full px-3 py-2 text-sm font-medium rounded-lg transition-all hover:bg-secondary/50",
|
|
333
|
+
"text-muted-foreground"
|
|
334
|
+
)}
|
|
335
|
+
style={{ paddingLeft: `${12 + depth * 16}px` }}
|
|
336
|
+
onClick={() => toggleFolder(node.path)}
|
|
337
|
+
>
|
|
338
|
+
{isExpanded ? (
|
|
339
|
+
<FolderOpen size={16} className="text-amber-500 shrink-0" />
|
|
340
|
+
) : (
|
|
341
|
+
<Folder size={16} className="text-amber-500 shrink-0" />
|
|
342
|
+
)}
|
|
343
|
+
<span className="truncate">{node.name}</span>
|
|
344
|
+
<span className="ml-auto text-[10px] text-muted-foreground/50">
|
|
345
|
+
{node.children?.length || 0}
|
|
346
|
+
</span>
|
|
347
|
+
</button>
|
|
348
|
+
{isExpanded &&
|
|
349
|
+
node.children &&
|
|
350
|
+
renderTree(node.children, depth + 1)}
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Determine file icon based on path/name
|
|
356
|
+
const getFileIcon = () => {
|
|
357
|
+
const pathLower = node.path.toLowerCase();
|
|
358
|
+
if (pathLower.includes("api") || pathLower.includes("technical"))
|
|
359
|
+
return <FileCode size={16} className="text-blue-400" />;
|
|
360
|
+
if (pathLower.includes("product") || pathLower.includes("prd"))
|
|
361
|
+
return <Lightbulb size={16} className="text-amber-400" />;
|
|
362
|
+
if (pathLower.includes("design"))
|
|
363
|
+
return <Palette size={16} className="text-purple-400" />;
|
|
364
|
+
return <FileText size={16} />;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<button
|
|
369
|
+
key={node.path}
|
|
370
|
+
className={cn(
|
|
371
|
+
"flex items-center gap-2 w-full px-3 py-2 text-sm font-medium rounded-lg transition-all",
|
|
372
|
+
isSelected
|
|
373
|
+
? "bg-primary text-primary-foreground"
|
|
374
|
+
: "text-muted-foreground hover:bg-secondary/50 hover:text-foreground"
|
|
375
|
+
)}
|
|
376
|
+
style={{ paddingLeft: `${12 + depth * 16}px` }}
|
|
377
|
+
onClick={() => setSelectedPath(node.path)}
|
|
378
|
+
>
|
|
379
|
+
<span className="shrink-0">{getFileIcon()}</span>
|
|
380
|
+
<span className="truncate">{node.name.replace(".md", "")}</span>
|
|
381
|
+
</button>
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
return (
|
|
387
|
+
<div className="flex gap-6 h-[calc(100vh-8rem)]">
|
|
388
|
+
{/* Sidebar */}
|
|
389
|
+
<aside className="w-80 flex flex-col bg-card/50 border border-border/50 rounded-xl overflow-hidden">
|
|
390
|
+
{/* Header */}
|
|
391
|
+
<div className="p-4 border-b border-border/50">
|
|
392
|
+
<div className="flex justify-between items-center mb-3">
|
|
393
|
+
<h3 className="text-sm font-bold text-foreground">Documentation</h3>
|
|
394
|
+
<Button
|
|
395
|
+
size="icon"
|
|
396
|
+
variant="secondary"
|
|
397
|
+
className="h-8 w-8"
|
|
398
|
+
onClick={() => setIsCreating(true)}
|
|
399
|
+
>
|
|
400
|
+
<Plus size={16} />
|
|
401
|
+
</Button>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
{/* Search */}
|
|
405
|
+
<div className="relative">
|
|
406
|
+
<Search
|
|
407
|
+
size={14}
|
|
408
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
|
|
409
|
+
/>
|
|
410
|
+
<Input
|
|
411
|
+
placeholder="Search docs..."
|
|
412
|
+
value={searchQuery}
|
|
413
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
414
|
+
className="h-9 pl-9 text-sm bg-secondary/40"
|
|
415
|
+
/>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
{/* Category Filters */}
|
|
420
|
+
<div className="p-3 border-b border-border/50">
|
|
421
|
+
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground uppercase tracking-wider mb-2 px-1">
|
|
422
|
+
<Filter size={12} />
|
|
423
|
+
Categories
|
|
424
|
+
</div>
|
|
425
|
+
<div className="flex flex-wrap gap-1.5">
|
|
426
|
+
{DOC_CATEGORIES.map((cat) => {
|
|
427
|
+
const Icon = cat.icon;
|
|
428
|
+
return (
|
|
429
|
+
<button
|
|
430
|
+
key={cat.id}
|
|
431
|
+
className={cn(
|
|
432
|
+
"flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-all",
|
|
433
|
+
activeCategory === cat.id
|
|
434
|
+
? `${cat.bgColor} ${cat.color}`
|
|
435
|
+
: "text-muted-foreground hover:bg-secondary/50"
|
|
436
|
+
)}
|
|
437
|
+
onClick={() => setActiveCategory(cat.id)}
|
|
438
|
+
>
|
|
439
|
+
<Icon size={12} />
|
|
440
|
+
{cat.label}
|
|
441
|
+
</button>
|
|
442
|
+
);
|
|
443
|
+
})}
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
{/* Create New Document Modal */}
|
|
448
|
+
{isCreating && (
|
|
449
|
+
<div className="p-4 bg-secondary/30 border-b border-border/50 animate-in fade-in slide-in-from-top-2 duration-200">
|
|
450
|
+
<div className="flex items-center justify-between mb-3">
|
|
451
|
+
<span className="text-xs font-semibold text-foreground">
|
|
452
|
+
New Document
|
|
453
|
+
</span>
|
|
454
|
+
<button
|
|
455
|
+
className="text-muted-foreground hover:text-foreground"
|
|
456
|
+
onClick={() => setIsCreating(false)}
|
|
457
|
+
>
|
|
458
|
+
<X size={14} />
|
|
459
|
+
</button>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<Input
|
|
463
|
+
autoFocus
|
|
464
|
+
placeholder="document-name"
|
|
465
|
+
value={newFileName}
|
|
466
|
+
onChange={(e) => setNewFileName(e.target.value)}
|
|
467
|
+
onKeyDown={(e) => {
|
|
468
|
+
if (e.key === "Enter") handleCreateFile();
|
|
469
|
+
if (e.key === "Escape") setIsCreating(false);
|
|
470
|
+
}}
|
|
471
|
+
className="h-9 mb-3"
|
|
472
|
+
/>
|
|
473
|
+
|
|
474
|
+
<div className="mb-3">
|
|
475
|
+
<label className="text-[10px] uppercase tracking-wider text-muted-foreground mb-2 block">
|
|
476
|
+
Template
|
|
477
|
+
</label>
|
|
478
|
+
<div className="grid grid-cols-2 gap-1.5">
|
|
479
|
+
{DOC_TEMPLATES.slice(0, 4).map((template) => (
|
|
480
|
+
<button
|
|
481
|
+
key={template.id}
|
|
482
|
+
className={cn(
|
|
483
|
+
"px-2.5 py-2 text-[11px] font-medium rounded-lg border transition-all text-left",
|
|
484
|
+
selectedTemplate === template.id
|
|
485
|
+
? "border-primary bg-primary/10 text-foreground"
|
|
486
|
+
: "border-border/50 text-muted-foreground hover:border-border hover:bg-secondary/30"
|
|
487
|
+
)}
|
|
488
|
+
onClick={() => setSelectedTemplate(template.id)}
|
|
489
|
+
>
|
|
490
|
+
{template.label}
|
|
491
|
+
</button>
|
|
492
|
+
))}
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<Button
|
|
497
|
+
size="sm"
|
|
498
|
+
className="w-full h-8"
|
|
499
|
+
onClick={handleCreateFile}
|
|
500
|
+
disabled={!newFileName.trim()}
|
|
501
|
+
>
|
|
502
|
+
Create Document
|
|
503
|
+
</Button>
|
|
504
|
+
</div>
|
|
505
|
+
)}
|
|
506
|
+
|
|
507
|
+
{/* File Tree */}
|
|
508
|
+
<div className="flex-1 overflow-y-auto p-2">
|
|
509
|
+
{filteredTree.length > 0 ? (
|
|
510
|
+
renderTree(filteredTree)
|
|
511
|
+
) : (
|
|
512
|
+
<div className="flex flex-col items-center justify-center h-full text-center p-4">
|
|
513
|
+
<File size={32} className="text-muted-foreground/30 mb-2" />
|
|
514
|
+
<p className="text-sm text-muted-foreground">No documents</p>
|
|
515
|
+
<p className="text-xs text-muted-foreground/60">
|
|
516
|
+
Create your first document
|
|
517
|
+
</p>
|
|
518
|
+
</div>
|
|
519
|
+
)}
|
|
520
|
+
</div>
|
|
521
|
+
</aside>
|
|
522
|
+
|
|
523
|
+
{/* Main Content */}
|
|
524
|
+
<main className="flex-1 flex flex-col min-w-0">
|
|
525
|
+
{selectedPath ? (
|
|
526
|
+
<div className="flex flex-col h-full gap-4" data-color-mode="dark">
|
|
527
|
+
{/* Document Header */}
|
|
528
|
+
<header className="flex justify-between items-center bg-card/50 border border-border/50 p-4 rounded-xl">
|
|
529
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
530
|
+
<div className="p-2.5 bg-primary/10 rounded-xl shrink-0">
|
|
531
|
+
<FileText size={18} className="text-primary" />
|
|
532
|
+
</div>
|
|
533
|
+
<div className="flex flex-col min-w-0">
|
|
534
|
+
<span className="font-semibold text-foreground truncate">
|
|
535
|
+
{selectedPath?.split("/").pop()?.replace(".md", "")}
|
|
536
|
+
</span>
|
|
537
|
+
<span className="text-[11px] text-muted-foreground truncate flex items-center gap-1.5">
|
|
538
|
+
<span className="opacity-60">/</span>
|
|
539
|
+
{selectedPath}
|
|
540
|
+
{hasUnsavedChanges && (
|
|
541
|
+
<span className="w-1.5 h-1.5 rounded-full bg-amber-500 ml-1" />
|
|
542
|
+
)}
|
|
543
|
+
</span>
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
<div className="flex items-center gap-3">
|
|
547
|
+
<div className="flex bg-secondary/50 p-1 rounded-lg border border-border/50">
|
|
548
|
+
<button
|
|
549
|
+
className={cn(
|
|
550
|
+
"px-3 py-1.5 text-xs font-semibold rounded-md transition-all",
|
|
551
|
+
contentMode === "edit"
|
|
552
|
+
? "bg-background text-foreground shadow-sm"
|
|
553
|
+
: "text-muted-foreground hover:text-foreground"
|
|
554
|
+
)}
|
|
555
|
+
onClick={() => setContentMode("edit")}
|
|
556
|
+
>
|
|
557
|
+
Edit
|
|
558
|
+
</button>
|
|
559
|
+
<button
|
|
560
|
+
className={cn(
|
|
561
|
+
"px-3 py-1.5 text-xs font-semibold rounded-md transition-all",
|
|
562
|
+
contentMode === "preview"
|
|
563
|
+
? "bg-background text-foreground shadow-sm"
|
|
564
|
+
: "text-muted-foreground hover:text-foreground"
|
|
565
|
+
)}
|
|
566
|
+
onClick={() => setContentMode("preview")}
|
|
567
|
+
>
|
|
568
|
+
Preview
|
|
569
|
+
</button>
|
|
570
|
+
</div>
|
|
571
|
+
<Button
|
|
572
|
+
onClick={handleSave}
|
|
573
|
+
className="h-9"
|
|
574
|
+
disabled={!hasUnsavedChanges}
|
|
575
|
+
>
|
|
576
|
+
<Save size={14} className="mr-2" />
|
|
577
|
+
Save
|
|
578
|
+
</Button>
|
|
579
|
+
</div>
|
|
580
|
+
</header>
|
|
581
|
+
|
|
582
|
+
{/* Editor */}
|
|
583
|
+
<div className="flex-1 bg-card/50 border border-border/50 rounded-xl overflow-hidden p-4 markdown-container">
|
|
584
|
+
<MDEditor
|
|
585
|
+
value={content}
|
|
586
|
+
onChange={(v) => setContent(v || "")}
|
|
587
|
+
height="100%"
|
|
588
|
+
preview={contentMode}
|
|
589
|
+
hideToolbar={contentMode === "preview"}
|
|
590
|
+
className="bg-transparent! border-none! text-foreground!"
|
|
591
|
+
/>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
) : (
|
|
595
|
+
<div className="h-full flex items-center justify-center">
|
|
596
|
+
<div className="max-w-lg w-full p-10 bg-card/50 border border-border/50 rounded-2xl flex flex-col items-center text-center space-y-5">
|
|
597
|
+
<div className="p-5 bg-secondary/50 rounded-2xl">
|
|
598
|
+
<BookOpen size={40} className="text-muted-foreground/40" />
|
|
599
|
+
</div>
|
|
600
|
+
<div>
|
|
601
|
+
<h2 className="text-xl font-bold text-foreground mb-2">
|
|
602
|
+
Welcome to Documentation
|
|
603
|
+
</h2>
|
|
604
|
+
<p className="text-muted-foreground text-sm leading-relaxed max-w-sm">
|
|
605
|
+
Select a document from the sidebar or create a new one using
|
|
606
|
+
templates for PRDs, technical specs, and more.
|
|
607
|
+
</p>
|
|
608
|
+
</div>
|
|
609
|
+
<div className="flex gap-2 pt-2">
|
|
610
|
+
<Button
|
|
611
|
+
variant="secondary"
|
|
612
|
+
size="sm"
|
|
613
|
+
onClick={() => setIsCreating(true)}
|
|
614
|
+
>
|
|
615
|
+
<Plus size={14} className="mr-1.5" />
|
|
616
|
+
New Document
|
|
617
|
+
</Button>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
)}
|
|
622
|
+
</main>
|
|
623
|
+
</div>
|
|
624
|
+
);
|
|
625
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"jsx": "preserve",
|
|
5
|
+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"plugins": [
|
|
11
|
+
{
|
|
12
|
+
"name": "next"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"paths": {
|
|
16
|
+
"@/*": ["./src/*"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"include": ["src", ".next/types/**/*.ts"],
|
|
20
|
+
"exclude": ["node_modules"]
|
|
21
|
+
}
|