beads-kanban-ui 0.1.0 → 0.1.1
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/README.md +16 -222
- package/package.json +18 -55
- package/.designs/beads-kanban-ui-bj0.md +0 -73
- package/.designs/beads-kanban-ui-qxq.md +0 -144
- package/.designs/epic-support.md +0 -282
- package/.env.local.example +0 -2
- package/.eslintrc.json +0 -3
- package/.gitattributes +0 -3
- package/.github/workflows/release.yml +0 -123
- package/.history/README_20260121193710.md +0 -227
- package/.history/README_20260121193918.md +0 -227
- package/.history/README_20260121193921.md +0 -227
- package/.history/README_20260121193933.md +0 -227
- package/.history/README_20260121193934.md +0 -227
- package/.history/README_20260121193944.md +0 -227
- package/.history/README_20260121193953.md +0 -227
- package/.history/src/app/page_20260121133429.tsx +0 -134
- package/.history/src/app/page_20260121133928.tsx +0 -134
- package/.history/src/app/page_20260121144850.tsx +0 -138
- package/.history/src/app/page_20260121144854.tsx +0 -138
- package/.history/src/app/page_20260121144858.tsx +0 -138
- package/.history/src/app/page_20260121144902.tsx +0 -138
- package/.history/src/app/page_20260121144906.tsx +0 -138
- package/.history/src/app/page_20260121144911.tsx +0 -138
- package/.history/src/app/page_20260121144928.tsx +0 -138
- package/.playwright-mcp/.playwright-mcp/morphing-dialog-wheel-scroll-fix.png +0 -0
- package/.playwright-mcp/beams-test.png +0 -0
- package/.playwright-mcp/card-verification.png +0 -0
- package/.playwright-mcp/design-doc-dialog-fix-verification.png +0 -0
- package/.playwright-mcp/dialog-width-test.png +0 -0
- package/.playwright-mcp/homepage.png +0 -0
- package/.playwright-mcp/morphing-dialog-expanded.png +0 -0
- package/.playwright-mcp/morphing-dialog-fixes-final.png +0 -0
- package/.playwright-mcp/morphing-dialog-open.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-08-31-529Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-09-23-431Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-10-28-773Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-10-47-432Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-11-12-350Z.png +0 -0
- package/.playwright-mcp/screenshot-after-click.png +0 -0
- package/.playwright-mcp/screenshot-after-dialog-click.png +0 -0
- package/.playwright-mcp/sheet-restored-after-dialog-close.png +0 -0
- package/.playwright-mcp/test-1-sheet-open-with-overlay.png +0 -0
- package/.playwright-mcp/test-2-morphing-dialog-with-overlay.png +0 -0
- package/.playwright-mcp/test-3-sheet-open-dark-overlay.png +0 -0
- package/.playwright-mcp/test-4-morphing-dialog-with-dark-overlay.png +0 -0
- package/.playwright-mcp/test-5-morphing-dialog-scrolled.png +0 -0
- package/.playwright-mcp/test-6-sheet-restored-after-dialog-close.png +0 -0
- package/.playwright-mcp/wheel-scroll-fixed.png +0 -0
- package/Screenshots/bead-detail.png +0 -0
- package/Screenshots/dashboard.png +0 -0
- package/Screenshots/kanban-board.png +0 -0
- package/components.json +0 -27
- package/logo/logo.svg +0 -1
- package/next.config.js +0 -9
- package/npm/README.md +0 -37
- package/npm/package.json +0 -20
- package/postcss.config.js +0 -6
- package/public/logo.svg +0 -1
- package/restart.sh +0 -5
- package/server/Cargo.lock +0 -1685
- package/server/Cargo.toml +0 -24
- package/server/src/db.rs +0 -570
- package/server/src/main.rs +0 -141
- package/server/src/routes/beads.rs +0 -413
- package/server/src/routes/cli.rs +0 -150
- package/server/src/routes/fs.rs +0 -360
- package/server/src/routes/git.rs +0 -169
- package/server/src/routes/mod.rs +0 -107
- package/server/src/routes/projects.rs +0 -177
- package/server/src/routes/watch.rs +0 -211
- package/src/app/globals.css +0 -101
- package/src/app/layout.tsx +0 -36
- package/src/app/page.tsx +0 -348
- package/src/app/project/kanban-board.tsx +0 -356
- package/src/app/project/page.tsx +0 -18
- package/src/app/settings/page.tsx +0 -224
- package/src/components/Beams.css +0 -5
- package/src/components/Beams.jsx +0 -307
- package/src/components/Galaxy.css +0 -5
- package/src/components/Galaxy.jsx +0 -333
- package/src/components/activity-timeline.tsx +0 -172
- package/src/components/add-project-dialog.tsx +0 -219
- package/src/components/bead-card.tsx +0 -196
- package/src/components/bead-detail.tsx +0 -306
- package/src/components/color-picker.tsx +0 -101
- package/src/components/comment-input.tsx +0 -155
- package/src/components/comment-list.tsx +0 -147
- package/src/components/dependency-badge.tsx +0 -106
- package/src/components/design-doc-dialog.tsx +0 -58
- package/src/components/design-doc-preview.tsx +0 -97
- package/src/components/design-doc-viewer.tsx +0 -199
- package/src/components/editable-project-name.tsx +0 -178
- package/src/components/epic-card.tsx +0 -263
- package/src/components/folder-browser.tsx +0 -273
- package/src/components/footer.tsx +0 -27
- package/src/components/kanban/default.tsx +0 -184
- package/src/components/kanban-column.tsx +0 -167
- package/src/components/project-card.tsx +0 -191
- package/src/components/quick-filter-bar.tsx +0 -279
- package/src/components/scan-directory-dialog.tsx +0 -368
- package/src/components/status-donut.tsx +0 -197
- package/src/components/subtask-list.tsx +0 -128
- package/src/components/tag-picker.tsx +0 -252
- package/src/components/ui/.gitkeep +0 -0
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/avatar.tsx +0 -67
- package/src/components/ui/badge.tsx +0 -230
- package/src/components/ui/button.tsx +0 -433
- package/src/components/ui/card/index.tsx +0 -24
- package/src/components/ui/card/roiui-card.module.css +0 -197
- package/src/components/ui/card/roiui-card.tsx +0 -154
- package/src/components/ui/card/shadcn-card.tsx +0 -76
- package/src/components/ui/chart.tsx +0 -369
- package/src/components/ui/dialog.tsx +0 -122
- package/src/components/ui/dropdown-menu.tsx +0 -201
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/kanban.tsx +0 -522
- package/src/components/ui/morphing-dialog.tsx +0 -457
- package/src/components/ui/popover.tsx +0 -33
- package/src/components/ui/progress.tsx +0 -28
- package/src/components/ui/scroll-area.tsx +0 -48
- package/src/components/ui/select.tsx +0 -159
- package/src/components/ui/separator.tsx +0 -31
- package/src/components/ui/sheet.tsx +0 -142
- package/src/components/ui/skeleton.tsx +0 -15
- package/src/components/ui/toast.tsx +0 -129
- package/src/components/ui/toaster.tsx +0 -35
- package/src/components/ui/tooltip.tsx +0 -30
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-bead-filters.ts +0 -261
- package/src/hooks/use-beads.ts +0 -162
- package/src/hooks/use-branch-statuses.ts +0 -161
- package/src/hooks/use-epics.ts +0 -173
- package/src/hooks/use-file-watcher.ts +0 -111
- package/src/hooks/use-keyboard-navigation.ts +0 -282
- package/src/hooks/use-project.ts +0 -61
- package/src/hooks/use-projects.ts +0 -93
- package/src/hooks/use-toast.ts +0 -194
- package/src/hooks/useClickOutside.tsx +0 -26
- package/src/lib/.gitkeep +0 -0
- package/src/lib/api.ts +0 -186
- package/src/lib/beads-parser.ts +0 -252
- package/src/lib/cli.ts +0 -193
- package/src/lib/db.ts +0 -145
- package/src/lib/design-doc.ts +0 -74
- package/src/lib/epic-parser.ts +0 -242
- package/src/lib/git.ts +0 -102
- package/src/lib/utils.ts +0 -12
- package/src/types/index.ts +0 -107
- package/tailwind.config.ts +0 -85
- package/tsconfig.json +0 -26
- /package/{npm/bin → bin}/cli.js +0 -0
- /package/{npm/scripts → scripts}/postinstall.js +0 -0
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useRef, useEffect } from "react";
|
|
4
|
-
import { Pencil, Check, X, Loader2 } from "lucide-react";
|
|
5
|
-
import { Button } from "@/components/ui/button";
|
|
6
|
-
import { Input } from "@/components/ui/input";
|
|
7
|
-
import {
|
|
8
|
-
Popover,
|
|
9
|
-
PopoverContent,
|
|
10
|
-
PopoverTrigger,
|
|
11
|
-
} from "@/components/ui/popover";
|
|
12
|
-
import { useToast } from "@/hooks/use-toast";
|
|
13
|
-
import { updateProject } from "@/lib/db";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Converts kebab-case, snake_case, camelCase to Title Case with spaces
|
|
17
|
-
*/
|
|
18
|
-
function formatProjectName(name: string): string {
|
|
19
|
-
return name
|
|
20
|
-
.replace(/[-_]/g, ' ') // Replace hyphens and underscores with spaces
|
|
21
|
-
.replace(/([a-z])([A-Z])/g, '$1 $2') // Add space before capitals in camelCase
|
|
22
|
-
.replace(/\b\w/g, c => c.toUpperCase()); // Capitalize first letter of each word
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface EditableProjectNameProps {
|
|
26
|
-
projectId: string;
|
|
27
|
-
initialName: string;
|
|
28
|
-
onNameUpdated: () => void;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Editable project name component with popover for inline editing
|
|
33
|
-
*/
|
|
34
|
-
export function EditableProjectName({
|
|
35
|
-
projectId,
|
|
36
|
-
initialName,
|
|
37
|
-
onNameUpdated,
|
|
38
|
-
}: EditableProjectNameProps) {
|
|
39
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
40
|
-
const [name, setName] = useState(initialName);
|
|
41
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
42
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
43
|
-
const { toast } = useToast();
|
|
44
|
-
|
|
45
|
-
// Reset name when popover opens
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (isOpen) {
|
|
48
|
-
setName(initialName);
|
|
49
|
-
// Focus input after popover opens
|
|
50
|
-
setTimeout(() => inputRef.current?.select(), 0);
|
|
51
|
-
}
|
|
52
|
-
}, [isOpen, initialName]);
|
|
53
|
-
|
|
54
|
-
const handleSubmit = async (e?: React.FormEvent) => {
|
|
55
|
-
e?.preventDefault();
|
|
56
|
-
|
|
57
|
-
const trimmedName = name.trim();
|
|
58
|
-
|
|
59
|
-
if (!trimmedName) {
|
|
60
|
-
toast({
|
|
61
|
-
title: "Invalid name",
|
|
62
|
-
description: "Project name cannot be empty.",
|
|
63
|
-
variant: "destructive",
|
|
64
|
-
});
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (trimmedName === initialName) {
|
|
69
|
-
setIsOpen(false);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
setIsSubmitting(true);
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
await updateProject({ id: projectId, name: trimmedName });
|
|
77
|
-
|
|
78
|
-
toast({
|
|
79
|
-
title: "Project renamed",
|
|
80
|
-
description: `Project has been renamed to "${trimmedName}".`,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
setIsOpen(false);
|
|
84
|
-
onNameUpdated();
|
|
85
|
-
} catch (err) {
|
|
86
|
-
console.error("Error updating project name:", err);
|
|
87
|
-
toast({
|
|
88
|
-
title: "Error",
|
|
89
|
-
description: "Failed to update project name. Please try again.",
|
|
90
|
-
variant: "destructive",
|
|
91
|
-
});
|
|
92
|
-
} finally {
|
|
93
|
-
setIsSubmitting(false);
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const handleCancel = () => {
|
|
98
|
-
setName(initialName);
|
|
99
|
-
setIsOpen(false);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
103
|
-
if (e.key === "Escape") {
|
|
104
|
-
handleCancel();
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<div className="flex items-center gap-2">
|
|
110
|
-
<h1 className="text-lg font-semibold text-zinc-100">{formatProjectName(initialName)}</h1>
|
|
111
|
-
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
112
|
-
<PopoverTrigger asChild>
|
|
113
|
-
<Button
|
|
114
|
-
variant="ghost"
|
|
115
|
-
size="icon"
|
|
116
|
-
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
|
117
|
-
aria-label="Edit project name"
|
|
118
|
-
>
|
|
119
|
-
<Pencil className="h-3.5 w-3.5" />
|
|
120
|
-
</Button>
|
|
121
|
-
</PopoverTrigger>
|
|
122
|
-
<PopoverContent className="w-80" align="start">
|
|
123
|
-
<form onSubmit={handleSubmit}>
|
|
124
|
-
<div className="space-y-4">
|
|
125
|
-
<div className="space-y-2">
|
|
126
|
-
<label
|
|
127
|
-
htmlFor="project-name"
|
|
128
|
-
className="text-sm font-medium text-foreground"
|
|
129
|
-
>
|
|
130
|
-
Project Name
|
|
131
|
-
</label>
|
|
132
|
-
<Input
|
|
133
|
-
ref={inputRef}
|
|
134
|
-
id="project-name"
|
|
135
|
-
value={name}
|
|
136
|
-
onChange={(e) => setName(e.target.value)}
|
|
137
|
-
onKeyDown={handleKeyDown}
|
|
138
|
-
placeholder="Enter project name"
|
|
139
|
-
disabled={isSubmitting}
|
|
140
|
-
autoComplete="off"
|
|
141
|
-
/>
|
|
142
|
-
</div>
|
|
143
|
-
<div className="flex justify-end gap-2">
|
|
144
|
-
<Button
|
|
145
|
-
type="button"
|
|
146
|
-
variant="outline"
|
|
147
|
-
size="sm"
|
|
148
|
-
onClick={handleCancel}
|
|
149
|
-
disabled={isSubmitting}
|
|
150
|
-
>
|
|
151
|
-
<X className="mr-1 h-3.5 w-3.5" />
|
|
152
|
-
Cancel
|
|
153
|
-
</Button>
|
|
154
|
-
<Button
|
|
155
|
-
type="submit"
|
|
156
|
-
size="sm"
|
|
157
|
-
disabled={isSubmitting || !name.trim()}
|
|
158
|
-
>
|
|
159
|
-
{isSubmitting ? (
|
|
160
|
-
<>
|
|
161
|
-
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
|
162
|
-
Saving...
|
|
163
|
-
</>
|
|
164
|
-
) : (
|
|
165
|
-
<>
|
|
166
|
-
<Check className="mr-1 h-3.5 w-3.5" />
|
|
167
|
-
Save
|
|
168
|
-
</>
|
|
169
|
-
)}
|
|
170
|
-
</Button>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
</form>
|
|
174
|
-
</PopoverContent>
|
|
175
|
-
</Popover>
|
|
176
|
-
</div>
|
|
177
|
-
);
|
|
178
|
-
}
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { Badge } from "@/components/ui/badge";
|
|
5
|
-
import { Progress } from "@/components/ui/progress";
|
|
6
|
-
import { cn } from "@/lib/utils";
|
|
7
|
-
import type { Bead, Epic, EpicProgress } from "@/types";
|
|
8
|
-
import { ChevronDown, ChevronRight, Layers, MessageSquare } from "lucide-react";
|
|
9
|
-
import { SubtaskList } from "@/components/subtask-list";
|
|
10
|
-
import { DependencyBadge } from "@/components/dependency-badge";
|
|
11
|
-
import { DesignDocPreview } from "@/components/design-doc-preview";
|
|
12
|
-
import { computeEpicProgress } from "@/lib/epic-parser";
|
|
13
|
-
|
|
14
|
-
export interface EpicCardProps {
|
|
15
|
-
/** Epic bead with children */
|
|
16
|
-
epic: Epic;
|
|
17
|
-
/** All beads to resolve children */
|
|
18
|
-
allBeads: Bead[];
|
|
19
|
-
/** Ticket number for display */
|
|
20
|
-
ticketNumber?: number;
|
|
21
|
-
/** Whether this epic is selected */
|
|
22
|
-
isSelected?: boolean;
|
|
23
|
-
/** Callback when selecting this epic */
|
|
24
|
-
onSelect: (epic: Epic) => void;
|
|
25
|
-
/** Callback when clicking a child task */
|
|
26
|
-
onChildClick: (child: Bead) => void;
|
|
27
|
-
/** Callback when navigating to a dependency */
|
|
28
|
-
onNavigateToDependency?: (beadId: string) => void;
|
|
29
|
-
/** Project root path for fetching design docs */
|
|
30
|
-
projectPath?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Format bead ID for display
|
|
35
|
-
*/
|
|
36
|
-
function formatBeadId(id: string): string {
|
|
37
|
-
if (id.startsWith("BD-") || id.startsWith("bd-")) {
|
|
38
|
-
return id.length > 10 ? 'BD-' + id.slice(-6) : id.toUpperCase();
|
|
39
|
-
}
|
|
40
|
-
const parts = id.split("-");
|
|
41
|
-
const shortId = parts[parts.length - 1];
|
|
42
|
-
return 'BD-' + shortId.slice(0, 6);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Truncate text to max length
|
|
47
|
-
*/
|
|
48
|
-
function truncate(text: string, maxLength: number): string {
|
|
49
|
-
if (text.length <= maxLength) return text;
|
|
50
|
-
return text.slice(0, maxLength).trim() + "…";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Compute epic progress from children
|
|
55
|
-
* Uses epic-parser utility for proper dependency resolution
|
|
56
|
-
*/
|
|
57
|
-
function computeProgress(epic: Epic, allBeads: Bead[]): EpicProgress {
|
|
58
|
-
return computeEpicProgress(epic, allBeads);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Get progress bar indicator color based on completion percentage
|
|
63
|
-
*/
|
|
64
|
-
function getProgressIndicatorClass(percentage: number): string {
|
|
65
|
-
if (percentage === 100) return "[&>*]:bg-green-500";
|
|
66
|
-
if (percentage >= 75) return "[&>*]:bg-green-500";
|
|
67
|
-
if (percentage >= 50) return "[&>*]:bg-blue-500";
|
|
68
|
-
if (percentage >= 25) return "[&>*]:bg-amber-500";
|
|
69
|
-
return "[&>*]:bg-purple-500";
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Larger epic card with distinctive styling
|
|
74
|
-
*/
|
|
75
|
-
export function EpicCard({
|
|
76
|
-
epic,
|
|
77
|
-
allBeads,
|
|
78
|
-
ticketNumber,
|
|
79
|
-
isSelected = false,
|
|
80
|
-
onSelect,
|
|
81
|
-
onChildClick,
|
|
82
|
-
onNavigateToDependency,
|
|
83
|
-
projectPath
|
|
84
|
-
}: EpicCardProps) {
|
|
85
|
-
const [isExpanded, setIsExpanded] = useState(false);
|
|
86
|
-
const [isDesignPreviewExpanded, setIsDesignPreviewExpanded] = useState(false);
|
|
87
|
-
|
|
88
|
-
// Resolve children from IDs
|
|
89
|
-
const children = (epic.children || [])
|
|
90
|
-
.map(childId => allBeads.find(b => b.id === childId))
|
|
91
|
-
.filter((b): b is Bead => b !== undefined);
|
|
92
|
-
|
|
93
|
-
const progress = computeProgress(epic, allBeads);
|
|
94
|
-
const progressPercentage = progress.total > 0
|
|
95
|
-
? Math.round((progress.completed / progress.total) * 100)
|
|
96
|
-
: 0;
|
|
97
|
-
|
|
98
|
-
const commentCount = (epic.comments ?? []).length;
|
|
99
|
-
const hasDesignDoc = !!epic.design_doc;
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<div
|
|
103
|
-
data-bead-id={epic.id}
|
|
104
|
-
role="button"
|
|
105
|
-
tabIndex={0}
|
|
106
|
-
aria-label={`Select epic: ${epic.title}`}
|
|
107
|
-
className={cn(
|
|
108
|
-
"rounded-lg cursor-pointer p-4",
|
|
109
|
-
"bg-zinc-900/70 backdrop-blur-md",
|
|
110
|
-
"border border-zinc-800/60 border-l-4 border-l-purple-500",
|
|
111
|
-
"shadow-sm shadow-black/20",
|
|
112
|
-
"transition-[transform,box-shadow,border-color] duration-200",
|
|
113
|
-
"hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/30",
|
|
114
|
-
"hover:border-zinc-700",
|
|
115
|
-
"focus:outline-none focus-visible:ring-2 focus-visible:ring-purple-400 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0a0a0a]",
|
|
116
|
-
isSelected && "ring-2 ring-purple-400 ring-offset-2 ring-offset-[#0a0a0a]"
|
|
117
|
-
)}
|
|
118
|
-
onClick={() => onSelect(epic)}
|
|
119
|
-
onKeyDown={(e) => {
|
|
120
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
121
|
-
e.preventDefault();
|
|
122
|
-
onSelect(epic);
|
|
123
|
-
}
|
|
124
|
-
}}
|
|
125
|
-
>
|
|
126
|
-
<div className="space-y-3">
|
|
127
|
-
{/* Header: Ticket # + Epic Icon + ID + Dependencies */}
|
|
128
|
-
<div className="flex items-center justify-between">
|
|
129
|
-
<div className="flex items-center gap-2">
|
|
130
|
-
<Layers className="h-4 w-4 text-purple-400" aria-hidden="true" />
|
|
131
|
-
<span className="text-xs font-mono text-zinc-400">
|
|
132
|
-
{ticketNumber !== undefined && (
|
|
133
|
-
<span className="font-semibold text-white">#{ticketNumber}</span>
|
|
134
|
-
)}
|
|
135
|
-
{ticketNumber !== undefined && " "}
|
|
136
|
-
{formatBeadId(epic.id)}
|
|
137
|
-
</span>
|
|
138
|
-
</div>
|
|
139
|
-
<div className="flex items-center gap-1.5">
|
|
140
|
-
<DependencyBadge
|
|
141
|
-
deps={epic.deps}
|
|
142
|
-
blockers={epic.blockers}
|
|
143
|
-
onNavigate={onNavigateToDependency}
|
|
144
|
-
/>
|
|
145
|
-
<Badge
|
|
146
|
-
variant="outline"
|
|
147
|
-
className="text-[10px] px-2 py-0.5 border-purple-500/30 text-purple-400 bg-purple-500/20 font-semibold"
|
|
148
|
-
>
|
|
149
|
-
EPIC
|
|
150
|
-
</Badge>
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
|
|
154
|
-
{/* Title */}
|
|
155
|
-
<h3 className="font-bold text-base leading-tight text-purple-100">
|
|
156
|
-
{truncate(epic.title, 60)}
|
|
157
|
-
</h3>
|
|
158
|
-
|
|
159
|
-
{/* Description */}
|
|
160
|
-
{epic.description && (
|
|
161
|
-
<p className="text-xs text-zinc-400 leading-relaxed">
|
|
162
|
-
{truncate(epic.description, 100)}
|
|
163
|
-
</p>
|
|
164
|
-
)}
|
|
165
|
-
|
|
166
|
-
{/* Progress Bar */}
|
|
167
|
-
<div className="space-y-1.5">
|
|
168
|
-
<div className="flex items-center justify-between text-xs">
|
|
169
|
-
<span className="text-zinc-400">
|
|
170
|
-
Progress: {progress.completed}/{progress.total} completed
|
|
171
|
-
</span>
|
|
172
|
-
<span className="font-semibold text-zinc-300">{progressPercentage}%</span>
|
|
173
|
-
</div>
|
|
174
|
-
<Progress
|
|
175
|
-
value={progressPercentage}
|
|
176
|
-
aria-label={`Epic progress: ${progress.completed} of ${progress.total} completed`}
|
|
177
|
-
className={cn(
|
|
178
|
-
"h-2 bg-zinc-800",
|
|
179
|
-
getProgressIndicatorClass(progressPercentage)
|
|
180
|
-
)}
|
|
181
|
-
/>
|
|
182
|
-
<div className="flex items-center gap-3 text-[10px] text-zinc-500">
|
|
183
|
-
<span className="flex items-center gap-1">
|
|
184
|
-
<div className="w-2 h-2 rounded-full bg-blue-500" aria-hidden="true" />
|
|
185
|
-
{progress.inProgress} in progress
|
|
186
|
-
</span>
|
|
187
|
-
{progress.blocked > 0 && (
|
|
188
|
-
<span className="flex items-center gap-1">
|
|
189
|
-
<div className="w-2 h-2 rounded-full bg-red-500" aria-hidden="true" />
|
|
190
|
-
{progress.blocked} blocked
|
|
191
|
-
</span>
|
|
192
|
-
)}
|
|
193
|
-
</div>
|
|
194
|
-
</div>
|
|
195
|
-
|
|
196
|
-
{/* Design Doc Preview */}
|
|
197
|
-
{hasDesignDoc && projectPath && (
|
|
198
|
-
<div className="pt-2 border-t border-zinc-700">
|
|
199
|
-
<button
|
|
200
|
-
onClick={(e) => {
|
|
201
|
-
e.stopPropagation();
|
|
202
|
-
setIsDesignPreviewExpanded(!isDesignPreviewExpanded);
|
|
203
|
-
}}
|
|
204
|
-
aria-expanded={isDesignPreviewExpanded}
|
|
205
|
-
aria-label={`${isDesignPreviewExpanded ? 'Collapse' : 'Expand'} design preview`}
|
|
206
|
-
className="flex items-center gap-1 text-xs font-semibold text-purple-400 hover:text-purple-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-400 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-900 rounded mb-2"
|
|
207
|
-
>
|
|
208
|
-
{isDesignPreviewExpanded ? (
|
|
209
|
-
<ChevronDown className="h-3.5 w-3.5" aria-hidden="true" />
|
|
210
|
-
) : (
|
|
211
|
-
<ChevronRight className="h-3.5 w-3.5" aria-hidden="true" />
|
|
212
|
-
)}
|
|
213
|
-
Design Preview
|
|
214
|
-
</button>
|
|
215
|
-
{isDesignPreviewExpanded && (
|
|
216
|
-
<DesignDocPreview
|
|
217
|
-
designDocPath={epic.design_doc!}
|
|
218
|
-
epicId={epic.id}
|
|
219
|
-
projectPath={projectPath}
|
|
220
|
-
/>
|
|
221
|
-
)}
|
|
222
|
-
</div>
|
|
223
|
-
)}
|
|
224
|
-
|
|
225
|
-
{/* Children Preview/List */}
|
|
226
|
-
<div className="pt-2 border-t border-zinc-700">
|
|
227
|
-
<button
|
|
228
|
-
onClick={(e) => {
|
|
229
|
-
e.stopPropagation();
|
|
230
|
-
setIsExpanded(!isExpanded);
|
|
231
|
-
}}
|
|
232
|
-
aria-expanded={isExpanded}
|
|
233
|
-
aria-label={`${isExpanded ? 'Collapse' : 'Expand'} child tasks`}
|
|
234
|
-
className="flex items-center gap-1 text-xs font-semibold text-purple-400 hover:text-purple-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-400 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-900 rounded mb-2"
|
|
235
|
-
>
|
|
236
|
-
{isExpanded ? (
|
|
237
|
-
<ChevronDown className="h-3.5 w-3.5" aria-hidden="true" />
|
|
238
|
-
) : (
|
|
239
|
-
<ChevronRight className="h-3.5 w-3.5" aria-hidden="true" />
|
|
240
|
-
)}
|
|
241
|
-
Child Tasks ({children.length})
|
|
242
|
-
</button>
|
|
243
|
-
<SubtaskList
|
|
244
|
-
childTasks={children}
|
|
245
|
-
onChildClick={onChildClick}
|
|
246
|
-
maxCollapsed={3}
|
|
247
|
-
isExpanded={isExpanded}
|
|
248
|
-
/>
|
|
249
|
-
</div>
|
|
250
|
-
|
|
251
|
-
{/* Footer: comment count */}
|
|
252
|
-
{commentCount > 0 && (
|
|
253
|
-
<div className="flex items-center pt-2">
|
|
254
|
-
<span className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
|
255
|
-
<MessageSquare className="h-3 w-3" aria-hidden="true" />
|
|
256
|
-
{commentCount} {commentCount === 1 ? "comment" : "comments"}
|
|
257
|
-
</span>
|
|
258
|
-
</div>
|
|
259
|
-
)}
|
|
260
|
-
</div>
|
|
261
|
-
</div>
|
|
262
|
-
);
|
|
263
|
-
}
|