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,172 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { cn } from "@/lib/utils";
|
|
4
|
-
import type { Bead, Comment } from "@/types";
|
|
5
|
-
|
|
6
|
-
export interface ActivityTimelineProps {
|
|
7
|
-
bead: Bead;
|
|
8
|
-
comments: Comment[];
|
|
9
|
-
childBeads?: Bead[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Timeline event types
|
|
14
|
-
*/
|
|
15
|
-
type TimelineEventType = "created" | "status_change" | "comment" | "branch" | "child_created" | "child_status_change";
|
|
16
|
-
|
|
17
|
-
interface TimelineEvent {
|
|
18
|
-
id: string;
|
|
19
|
-
type: TimelineEventType;
|
|
20
|
-
description: string;
|
|
21
|
-
timestamp: Date;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Format a date for display (e.g., "Jan 12, 10:57 AM")
|
|
26
|
-
*/
|
|
27
|
-
function formatTimestamp(date: Date): string {
|
|
28
|
-
return date.toLocaleDateString("en-US", {
|
|
29
|
-
month: "short",
|
|
30
|
-
day: "numeric",
|
|
31
|
-
}) + ", " + date.toLocaleTimeString("en-US", {
|
|
32
|
-
hour: "numeric",
|
|
33
|
-
minute: "2-digit",
|
|
34
|
-
hour12: true,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Truncate a string to maxLength characters with ellipsis
|
|
40
|
-
*/
|
|
41
|
-
function truncateTitle(title: string, maxLength: number = 30): string {
|
|
42
|
-
if (title.length <= maxLength) return title;
|
|
43
|
-
return title.slice(0, maxLength - 1) + "\u2026";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Build timeline events from bead and comments
|
|
48
|
-
*/
|
|
49
|
-
function buildTimelineEvents(bead: Bead, comments: Comment[], childBeads: Bead[] = []): TimelineEvent[] {
|
|
50
|
-
const events: TimelineEvent[] = [];
|
|
51
|
-
|
|
52
|
-
// Created event
|
|
53
|
-
const createdAt = new Date(bead.created_at);
|
|
54
|
-
events.push({
|
|
55
|
-
id: `created-${bead.id}`,
|
|
56
|
-
type: "created",
|
|
57
|
-
description: "Created",
|
|
58
|
-
timestamp: createdAt,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// Status change event (if updated_at differs from created_at)
|
|
62
|
-
const updatedAt = new Date(bead.updated_at);
|
|
63
|
-
if (updatedAt.getTime() !== createdAt.getTime()) {
|
|
64
|
-
events.push({
|
|
65
|
-
id: `status-${bead.id}`,
|
|
66
|
-
type: "status_change",
|
|
67
|
-
description: `Status \u2192 ${bead.status}`,
|
|
68
|
-
timestamp: updatedAt,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Comment events
|
|
73
|
-
comments.forEach((comment) => {
|
|
74
|
-
events.push({
|
|
75
|
-
id: `comment-${comment.id}`,
|
|
76
|
-
type: "comment",
|
|
77
|
-
description: "Comment added",
|
|
78
|
-
timestamp: new Date(comment.created_at),
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// Child task events
|
|
83
|
-
childBeads.forEach((child) => {
|
|
84
|
-
const childCreatedAt = new Date(child.created_at);
|
|
85
|
-
const childUpdatedAt = new Date(child.updated_at);
|
|
86
|
-
const truncatedTitle = truncateTitle(child.title);
|
|
87
|
-
|
|
88
|
-
// Child created event
|
|
89
|
-
events.push({
|
|
90
|
-
id: `child-created-${child.id}`,
|
|
91
|
-
type: "child_created",
|
|
92
|
-
description: `Task created: ${truncatedTitle}`,
|
|
93
|
-
timestamp: childCreatedAt,
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Child status change event (if updated_at differs from created_at)
|
|
97
|
-
if (childUpdatedAt.getTime() !== childCreatedAt.getTime()) {
|
|
98
|
-
events.push({
|
|
99
|
-
id: `child-status-${child.id}`,
|
|
100
|
-
type: "child_status_change",
|
|
101
|
-
description: `Task \u2192 ${child.status}: ${truncatedTitle}`,
|
|
102
|
-
timestamp: childUpdatedAt,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Sort chronologically (oldest first)
|
|
108
|
-
events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
109
|
-
|
|
110
|
-
return events;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Activity Timeline component
|
|
115
|
-
* Displays a vertical timeline of bead activity events
|
|
116
|
-
*/
|
|
117
|
-
export function ActivityTimeline({ bead, comments, childBeads = [] }: ActivityTimelineProps) {
|
|
118
|
-
const events = buildTimelineEvents(bead, comments, childBeads);
|
|
119
|
-
|
|
120
|
-
if (events.length === 0) {
|
|
121
|
-
return (
|
|
122
|
-
<div className="mt-6 text-sm text-muted-foreground">
|
|
123
|
-
No activity recorded
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<div className="mt-6 space-y-1">
|
|
130
|
-
<h4 className="text-sm font-semibold text-foreground mb-3">
|
|
131
|
-
Activity Timeline
|
|
132
|
-
</h4>
|
|
133
|
-
<div className="relative">
|
|
134
|
-
{/* Vertical line */}
|
|
135
|
-
<div className="absolute left-[5px] top-2 bottom-2 w-px bg-zinc-700" />
|
|
136
|
-
|
|
137
|
-
{/* Events */}
|
|
138
|
-
<div className="space-y-3">
|
|
139
|
-
{events.map((event, index) => (
|
|
140
|
-
<div
|
|
141
|
-
key={event.id}
|
|
142
|
-
className="relative flex items-start gap-3 pl-5"
|
|
143
|
-
>
|
|
144
|
-
{/* Dot */}
|
|
145
|
-
<div
|
|
146
|
-
className={cn(
|
|
147
|
-
"absolute left-0 top-1.5 h-[11px] w-[11px] rounded-full border-2 bg-background",
|
|
148
|
-
event.type === "created" && "border-green-500",
|
|
149
|
-
event.type === "status_change" && "border-blue-500",
|
|
150
|
-
event.type === "comment" && "border-zinc-400",
|
|
151
|
-
event.type === "branch" && "border-purple-500",
|
|
152
|
-
event.type === "child_created" && "border-cyan-500",
|
|
153
|
-
event.type === "child_status_change" && "border-cyan-400"
|
|
154
|
-
)}
|
|
155
|
-
/>
|
|
156
|
-
|
|
157
|
-
{/* Content */}
|
|
158
|
-
<div className="flex flex-1 items-center justify-between min-w-0">
|
|
159
|
-
<span className="text-xs text-zinc-400 truncate">
|
|
160
|
-
{event.description}
|
|
161
|
-
</span>
|
|
162
|
-
<span className="text-[10px] text-zinc-600 whitespace-nowrap ml-2">
|
|
163
|
-
{formatTimestamp(event.timestamp)}
|
|
164
|
-
</span>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
))}
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
);
|
|
172
|
-
}
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import {
|
|
5
|
-
Dialog,
|
|
6
|
-
DialogContent,
|
|
7
|
-
DialogDescription,
|
|
8
|
-
DialogFooter,
|
|
9
|
-
DialogHeader,
|
|
10
|
-
DialogTitle,
|
|
11
|
-
} from "@/components/ui/dialog";
|
|
12
|
-
import { Button } from "@/components/ui/button";
|
|
13
|
-
import { Input } from "@/components/ui/input";
|
|
14
|
-
import { useToast } from "@/hooks/use-toast";
|
|
15
|
-
import * as api from "@/lib/api";
|
|
16
|
-
import type { CreateProjectInput } from "@/lib/db";
|
|
17
|
-
import { Folder, Loader2 } from "lucide-react";
|
|
18
|
-
|
|
19
|
-
interface AddProjectDialogProps {
|
|
20
|
-
open: boolean;
|
|
21
|
-
onOpenChange: (open: boolean) => void;
|
|
22
|
-
onAddProject: (input: CreateProjectInput) => Promise<void>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function AddProjectDialog({
|
|
26
|
-
open: isOpen,
|
|
27
|
-
onOpenChange,
|
|
28
|
-
onAddProject,
|
|
29
|
-
}: AddProjectDialogProps) {
|
|
30
|
-
const [projectPath, setProjectPath] = useState<string>("");
|
|
31
|
-
const [projectName, setProjectName] = useState<string>("");
|
|
32
|
-
const [isValidating, setIsValidating] = useState(false);
|
|
33
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
34
|
-
const [pathError, setPathError] = useState<string | null>(null);
|
|
35
|
-
const [showNameInput, setShowNameInput] = useState(false);
|
|
36
|
-
const { toast } = useToast();
|
|
37
|
-
|
|
38
|
-
const resetState = () => {
|
|
39
|
-
setProjectPath("");
|
|
40
|
-
setProjectName("");
|
|
41
|
-
setPathError(null);
|
|
42
|
-
setShowNameInput(false);
|
|
43
|
-
setIsValidating(false);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const handleOpenChange = (open: boolean) => {
|
|
47
|
-
if (!open) {
|
|
48
|
-
resetState();
|
|
49
|
-
}
|
|
50
|
-
onOpenChange(open);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const validateAndProceed = async () => {
|
|
54
|
-
if (!projectPath.trim()) {
|
|
55
|
-
setPathError("Please enter a project path.");
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
setIsValidating(true);
|
|
60
|
-
setPathError(null);
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const cleanPath = projectPath.trim().replace(/\/+$/, "");
|
|
64
|
-
const result = await api.fs.exists(`${cleanPath}/.beads`);
|
|
65
|
-
|
|
66
|
-
if (!result.exists) {
|
|
67
|
-
setPathError("No .beads folder found. Run `bd init` in your project first.");
|
|
68
|
-
toast({
|
|
69
|
-
title: "No .beads folder found",
|
|
70
|
-
description: "Run `bd init` in your project first.",
|
|
71
|
-
variant: "destructive",
|
|
72
|
-
});
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Extract folder name as default project name
|
|
77
|
-
const pathParts = cleanPath.split(/[/\\]/);
|
|
78
|
-
const defaultName = pathParts[pathParts.length - 1] || "Untitled Project";
|
|
79
|
-
|
|
80
|
-
setProjectPath(cleanPath);
|
|
81
|
-
setProjectName(defaultName);
|
|
82
|
-
setShowNameInput(true);
|
|
83
|
-
} catch (err) {
|
|
84
|
-
console.error("Error validating path:", err);
|
|
85
|
-
setPathError("Could not access the specified path. Please check it exists.");
|
|
86
|
-
} finally {
|
|
87
|
-
setIsValidating(false);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
92
|
-
e.preventDefault();
|
|
93
|
-
|
|
94
|
-
if (!projectPath || !projectName.trim()) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
setIsSubmitting(true);
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
await onAddProject({
|
|
102
|
-
name: projectName.trim(),
|
|
103
|
-
path: projectPath,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
toast({
|
|
107
|
-
title: "Project added",
|
|
108
|
-
description: `"${projectName}" has been added successfully.`,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
resetState();
|
|
112
|
-
onOpenChange(false);
|
|
113
|
-
} catch (err) {
|
|
114
|
-
console.error("Error adding project:", err);
|
|
115
|
-
toast({
|
|
116
|
-
title: "Error",
|
|
117
|
-
description: "Failed to add project. Please try again.",
|
|
118
|
-
variant: "destructive",
|
|
119
|
-
});
|
|
120
|
-
} finally {
|
|
121
|
-
setIsSubmitting(false);
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
|
127
|
-
<DialogContent className="sm:max-w-md">
|
|
128
|
-
<DialogHeader>
|
|
129
|
-
<DialogTitle>Add Project</DialogTitle>
|
|
130
|
-
<DialogDescription>
|
|
131
|
-
{showNameInput
|
|
132
|
-
? "Give your project a name."
|
|
133
|
-
: "Enter the path to a folder containing a beads project."}
|
|
134
|
-
</DialogDescription>
|
|
135
|
-
</DialogHeader>
|
|
136
|
-
|
|
137
|
-
{!showNameInput ? (
|
|
138
|
-
<div className="flex flex-col gap-4 py-4">
|
|
139
|
-
<div className="space-y-2">
|
|
140
|
-
<label htmlFor="path" className="text-sm font-medium text-zinc-300">
|
|
141
|
-
Project Path
|
|
142
|
-
</label>
|
|
143
|
-
<div className="relative">
|
|
144
|
-
<Folder className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-500" aria-hidden="true" />
|
|
145
|
-
<Input
|
|
146
|
-
id="path"
|
|
147
|
-
value={projectPath}
|
|
148
|
-
onChange={(e) => {
|
|
149
|
-
setProjectPath(e.target.value);
|
|
150
|
-
setPathError(null);
|
|
151
|
-
}}
|
|
152
|
-
placeholder="/path/to/your/project"
|
|
153
|
-
className="pl-10"
|
|
154
|
-
autoFocus
|
|
155
|
-
/>
|
|
156
|
-
</div>
|
|
157
|
-
{pathError && (
|
|
158
|
-
<p className="text-sm text-red-400">{pathError}</p>
|
|
159
|
-
)}
|
|
160
|
-
<p className="text-xs text-zinc-500">
|
|
161
|
-
Enter the full path to your project folder (must contain a .beads folder).
|
|
162
|
-
</p>
|
|
163
|
-
</div>
|
|
164
|
-
<DialogFooter>
|
|
165
|
-
<Button
|
|
166
|
-
onClick={validateAndProceed}
|
|
167
|
-
disabled={!projectPath.trim() || isValidating}
|
|
168
|
-
>
|
|
169
|
-
{isValidating ? (
|
|
170
|
-
<>
|
|
171
|
-
<Loader2 className="size-4 animate-spin" aria-hidden="true" />
|
|
172
|
-
Validating...
|
|
173
|
-
</>
|
|
174
|
-
) : (
|
|
175
|
-
"Continue"
|
|
176
|
-
)}
|
|
177
|
-
</Button>
|
|
178
|
-
</DialogFooter>
|
|
179
|
-
</div>
|
|
180
|
-
) : (
|
|
181
|
-
<form onSubmit={handleSubmit}>
|
|
182
|
-
<div className="space-y-4 py-4">
|
|
183
|
-
<div className="space-y-2">
|
|
184
|
-
<label htmlFor="name" className="text-sm font-medium text-zinc-300">
|
|
185
|
-
Project Name
|
|
186
|
-
</label>
|
|
187
|
-
<Input
|
|
188
|
-
id="name"
|
|
189
|
-
value={projectName}
|
|
190
|
-
onChange={(e) => setProjectName(e.target.value)}
|
|
191
|
-
placeholder="My Project"
|
|
192
|
-
autoFocus
|
|
193
|
-
/>
|
|
194
|
-
</div>
|
|
195
|
-
<div className="space-y-2">
|
|
196
|
-
<label className="text-sm font-medium text-zinc-300">Location</label>
|
|
197
|
-
<p className="truncate rounded-md bg-zinc-800 px-3 py-2 text-sm text-zinc-400">
|
|
198
|
-
{projectPath}
|
|
199
|
-
</p>
|
|
200
|
-
</div>
|
|
201
|
-
</div>
|
|
202
|
-
<DialogFooter className="gap-2">
|
|
203
|
-
<Button
|
|
204
|
-
type="button"
|
|
205
|
-
variant="outline"
|
|
206
|
-
onClick={() => setShowNameInput(false)}
|
|
207
|
-
>
|
|
208
|
-
Back
|
|
209
|
-
</Button>
|
|
210
|
-
<Button type="submit" disabled={isSubmitting || !projectName.trim()}>
|
|
211
|
-
{isSubmitting ? "Adding..." : "Add Project"}
|
|
212
|
-
</Button>
|
|
213
|
-
</DialogFooter>
|
|
214
|
-
</form>
|
|
215
|
-
)}
|
|
216
|
-
</DialogContent>
|
|
217
|
-
</Dialog>
|
|
218
|
-
);
|
|
219
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Badge } from "@/components/ui/badge";
|
|
4
|
-
import { cn } from "@/lib/utils";
|
|
5
|
-
import type { Bead } from "@/types";
|
|
6
|
-
import type { BranchStatus } from "@/lib/git";
|
|
7
|
-
import { GitBranch, MessageSquare } from "lucide-react";
|
|
8
|
-
|
|
9
|
-
export interface BeadCardProps {
|
|
10
|
-
bead: Bead;
|
|
11
|
-
ticketNumber?: number;
|
|
12
|
-
branchStatus?: BranchStatus;
|
|
13
|
-
isSelected?: boolean;
|
|
14
|
-
onSelect: (bead: Bead) => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Get branch badge color based on ahead/behind status
|
|
19
|
-
* Dark theme variant with semi-transparent backgrounds
|
|
20
|
-
* Green: ahead only (has new commits, up to date with main)
|
|
21
|
-
* Yellow: behind only (main has new commits)
|
|
22
|
-
* Red: diverged (both ahead and behind)
|
|
23
|
-
* Default green: up to date (ahead=0, behind=0)
|
|
24
|
-
*/
|
|
25
|
-
function getBranchBadgeColor(status: BranchStatus): string {
|
|
26
|
-
const { ahead, behind } = status;
|
|
27
|
-
|
|
28
|
-
if (ahead > 0 && behind > 0) {
|
|
29
|
-
// Diverged - red
|
|
30
|
-
return "bg-red-500/10 text-red-400 border-red-600/30";
|
|
31
|
-
} else if (behind > 0) {
|
|
32
|
-
// Behind main - yellow
|
|
33
|
-
return "bg-yellow-500/10 text-yellow-400 border-yellow-600/30";
|
|
34
|
-
} else {
|
|
35
|
-
// Ahead only or up to date - green
|
|
36
|
-
return "bg-green-500/10 text-green-400 border-green-600/30";
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Format branch status display
|
|
42
|
-
* Returns: "bd-ID" if up to date, "bd-ID +ahead -behind" otherwise
|
|
43
|
-
*/
|
|
44
|
-
function formatBranchStatus(beadId: string, status: BranchStatus): string {
|
|
45
|
-
const formattedId = `bd-${formatBeadId(beadId)}`;
|
|
46
|
-
const { ahead, behind } = status;
|
|
47
|
-
|
|
48
|
-
if (ahead === 0 && behind === 0) {
|
|
49
|
-
return formattedId;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const parts = [formattedId];
|
|
53
|
-
if (ahead > 0) {
|
|
54
|
-
parts.push(`+${ahead}`);
|
|
55
|
-
}
|
|
56
|
-
if (behind > 0) {
|
|
57
|
-
parts.push(`-${behind}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return parts.join(" ");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Detect if bead is blocked by checking for unresolved dependencies
|
|
65
|
-
* A task is blocked only if it has unresolved dependencies
|
|
66
|
-
* Closed tasks are never blocked (they've completed)
|
|
67
|
-
* Note: The deps field only contains UNRESOLVED dependencies (backend filters out closed deps)
|
|
68
|
-
*/
|
|
69
|
-
function isBlocked(bead: Bead): boolean {
|
|
70
|
-
// Closed tasks are never blocked (they've completed)
|
|
71
|
-
if (bead.status === 'closed') return false;
|
|
72
|
-
// A task is blocked if it has unresolved dependencies
|
|
73
|
-
return (bead.deps ?? []).length > 0;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Truncate text to a maximum length with ellipsis
|
|
78
|
-
*/
|
|
79
|
-
function truncate(text: string, maxLength: number): string {
|
|
80
|
-
if (text.length <= maxLength) return text;
|
|
81
|
-
return text.slice(0, maxLength).trim() + "…";
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Format bead ID for display (short form)
|
|
86
|
-
*/
|
|
87
|
-
function formatBeadId(id: string): string {
|
|
88
|
-
// If ID is like "project-abc123", show "BD-abc123"
|
|
89
|
-
// If already has BD prefix, show as-is but truncate if needed
|
|
90
|
-
if (id.startsWith("BD-") || id.startsWith("bd-")) {
|
|
91
|
-
return id.length > 10 ? `BD-${id.slice(-6)}` : id.toUpperCase();
|
|
92
|
-
}
|
|
93
|
-
// Extract last part after dash
|
|
94
|
-
const parts = id.split("-");
|
|
95
|
-
const shortId = parts[parts.length - 1];
|
|
96
|
-
return `BD-${shortId.slice(0, 6)}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function BeadCard({ bead, ticketNumber, branchStatus, isSelected = false, onSelect }: BeadCardProps) {
|
|
100
|
-
const blocked = isBlocked(bead);
|
|
101
|
-
const commentCount = (bead.comments ?? []).length;
|
|
102
|
-
const branchExists = branchStatus?.exists ?? false;
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<div
|
|
106
|
-
data-bead-id={bead.id}
|
|
107
|
-
role="button"
|
|
108
|
-
tabIndex={0}
|
|
109
|
-
aria-label={`Select bead: ${bead.title}`}
|
|
110
|
-
className={cn(
|
|
111
|
-
"rounded-lg cursor-pointer p-4",
|
|
112
|
-
"bg-zinc-900/70 backdrop-blur-md",
|
|
113
|
-
"border border-zinc-800/60",
|
|
114
|
-
"shadow-sm shadow-black/20",
|
|
115
|
-
"transition-[transform,box-shadow,border-color] duration-200",
|
|
116
|
-
"hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/30",
|
|
117
|
-
"hover:border-zinc-700",
|
|
118
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0a0a0a]",
|
|
119
|
-
blocked ? "border-l-4 border-l-red-500" : "border-l-4 border-l-transparent",
|
|
120
|
-
isSelected && "ring-2 ring-zinc-400 ring-offset-2 ring-offset-[#0a0a0a]"
|
|
121
|
-
)}
|
|
122
|
-
onClick={() => onSelect(bead)}
|
|
123
|
-
onKeyDown={(e) => {
|
|
124
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
125
|
-
e.preventDefault();
|
|
126
|
-
onSelect(bead);
|
|
127
|
-
}
|
|
128
|
-
}}
|
|
129
|
-
>
|
|
130
|
-
<div className="space-y-3">
|
|
131
|
-
{/* Header: Ticket # + ID + Priority + Blocked badge */}
|
|
132
|
-
<div className="flex items-center justify-between">
|
|
133
|
-
<span className="text-xs font-mono text-zinc-500">
|
|
134
|
-
{ticketNumber !== undefined && (
|
|
135
|
-
<span className="font-semibold text-zinc-100">#{ticketNumber}</span>
|
|
136
|
-
)}
|
|
137
|
-
{ticketNumber !== undefined && " "}
|
|
138
|
-
{formatBeadId(bead.id)}
|
|
139
|
-
</span>
|
|
140
|
-
<div className="flex items-center gap-1.5">
|
|
141
|
-
{blocked && (
|
|
142
|
-
<Badge
|
|
143
|
-
className="text-[10px] px-1.5 py-0 bg-red-500/20 text-red-400 border border-red-500/30"
|
|
144
|
-
>
|
|
145
|
-
BLOCKED
|
|
146
|
-
</Badge>
|
|
147
|
-
)}
|
|
148
|
-
<Badge
|
|
149
|
-
className="text-[10px] px-1.5 py-0 bg-cyan-500/20 text-cyan-400 border border-cyan-500/30"
|
|
150
|
-
>
|
|
151
|
-
Task
|
|
152
|
-
</Badge>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
|
|
156
|
-
{/* Title */}
|
|
157
|
-
<h3 className="font-semibold text-sm leading-tight text-zinc-100">
|
|
158
|
-
{truncate(bead.title, 60)}
|
|
159
|
-
</h3>
|
|
160
|
-
|
|
161
|
-
{/* Description (truncated, muted) */}
|
|
162
|
-
{bead.description && (
|
|
163
|
-
<p className="text-xs text-zinc-400 leading-relaxed">
|
|
164
|
-
{truncate(bead.description, 80)}
|
|
165
|
-
</p>
|
|
166
|
-
)}
|
|
167
|
-
|
|
168
|
-
{/* Footer: comment count */}
|
|
169
|
-
{commentCount > 0 && (
|
|
170
|
-
<div className="flex items-center pt-1">
|
|
171
|
-
<span className="flex items-center gap-1 text-[10px] text-zinc-500">
|
|
172
|
-
<MessageSquare className="h-3 w-3" aria-hidden="true" />
|
|
173
|
-
{commentCount} {commentCount === 1 ? "comment" : "comments"}
|
|
174
|
-
</span>
|
|
175
|
-
</div>
|
|
176
|
-
)}
|
|
177
|
-
|
|
178
|
-
{/* Branch badge with ahead/behind status */}
|
|
179
|
-
{branchExists && branchStatus && (
|
|
180
|
-
<div className="pt-1">
|
|
181
|
-
<Badge
|
|
182
|
-
variant="outline"
|
|
183
|
-
className={cn(
|
|
184
|
-
"text-[10px] px-2 py-0.5 font-mono",
|
|
185
|
-
getBranchBadgeColor(branchStatus)
|
|
186
|
-
)}
|
|
187
|
-
>
|
|
188
|
-
<GitBranch className="h-3 w-3 mr-1" aria-hidden="true" />
|
|
189
|
-
{formatBranchStatus(bead.id, branchStatus)}
|
|
190
|
-
</Badge>
|
|
191
|
-
</div>
|
|
192
|
-
)}
|
|
193
|
-
</div>
|
|
194
|
-
</div>
|
|
195
|
-
);
|
|
196
|
-
}
|