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.
Files changed (154) hide show
  1. package/README.md +16 -222
  2. package/package.json +18 -55
  3. package/.designs/beads-kanban-ui-bj0.md +0 -73
  4. package/.designs/beads-kanban-ui-qxq.md +0 -144
  5. package/.designs/epic-support.md +0 -282
  6. package/.env.local.example +0 -2
  7. package/.eslintrc.json +0 -3
  8. package/.gitattributes +0 -3
  9. package/.github/workflows/release.yml +0 -123
  10. package/.history/README_20260121193710.md +0 -227
  11. package/.history/README_20260121193918.md +0 -227
  12. package/.history/README_20260121193921.md +0 -227
  13. package/.history/README_20260121193933.md +0 -227
  14. package/.history/README_20260121193934.md +0 -227
  15. package/.history/README_20260121193944.md +0 -227
  16. package/.history/README_20260121193953.md +0 -227
  17. package/.history/src/app/page_20260121133429.tsx +0 -134
  18. package/.history/src/app/page_20260121133928.tsx +0 -134
  19. package/.history/src/app/page_20260121144850.tsx +0 -138
  20. package/.history/src/app/page_20260121144854.tsx +0 -138
  21. package/.history/src/app/page_20260121144858.tsx +0 -138
  22. package/.history/src/app/page_20260121144902.tsx +0 -138
  23. package/.history/src/app/page_20260121144906.tsx +0 -138
  24. package/.history/src/app/page_20260121144911.tsx +0 -138
  25. package/.history/src/app/page_20260121144928.tsx +0 -138
  26. package/.playwright-mcp/.playwright-mcp/morphing-dialog-wheel-scroll-fix.png +0 -0
  27. package/.playwright-mcp/beams-test.png +0 -0
  28. package/.playwright-mcp/card-verification.png +0 -0
  29. package/.playwright-mcp/design-doc-dialog-fix-verification.png +0 -0
  30. package/.playwright-mcp/dialog-width-test.png +0 -0
  31. package/.playwright-mcp/homepage.png +0 -0
  32. package/.playwright-mcp/morphing-dialog-expanded.png +0 -0
  33. package/.playwright-mcp/morphing-dialog-fixes-final.png +0 -0
  34. package/.playwright-mcp/morphing-dialog-open.png +0 -0
  35. package/.playwright-mcp/page-2026-01-21T14-08-31-529Z.png +0 -0
  36. package/.playwright-mcp/page-2026-01-21T14-09-23-431Z.png +0 -0
  37. package/.playwright-mcp/page-2026-01-21T14-10-28-773Z.png +0 -0
  38. package/.playwright-mcp/page-2026-01-21T14-10-47-432Z.png +0 -0
  39. package/.playwright-mcp/page-2026-01-21T14-11-12-350Z.png +0 -0
  40. package/.playwright-mcp/screenshot-after-click.png +0 -0
  41. package/.playwright-mcp/screenshot-after-dialog-click.png +0 -0
  42. package/.playwright-mcp/sheet-restored-after-dialog-close.png +0 -0
  43. package/.playwright-mcp/test-1-sheet-open-with-overlay.png +0 -0
  44. package/.playwright-mcp/test-2-morphing-dialog-with-overlay.png +0 -0
  45. package/.playwright-mcp/test-3-sheet-open-dark-overlay.png +0 -0
  46. package/.playwright-mcp/test-4-morphing-dialog-with-dark-overlay.png +0 -0
  47. package/.playwright-mcp/test-5-morphing-dialog-scrolled.png +0 -0
  48. package/.playwright-mcp/test-6-sheet-restored-after-dialog-close.png +0 -0
  49. package/.playwright-mcp/wheel-scroll-fixed.png +0 -0
  50. package/Screenshots/bead-detail.png +0 -0
  51. package/Screenshots/dashboard.png +0 -0
  52. package/Screenshots/kanban-board.png +0 -0
  53. package/components.json +0 -27
  54. package/logo/logo.svg +0 -1
  55. package/next.config.js +0 -9
  56. package/npm/README.md +0 -37
  57. package/npm/package.json +0 -20
  58. package/postcss.config.js +0 -6
  59. package/public/logo.svg +0 -1
  60. package/restart.sh +0 -5
  61. package/server/Cargo.lock +0 -1685
  62. package/server/Cargo.toml +0 -24
  63. package/server/src/db.rs +0 -570
  64. package/server/src/main.rs +0 -141
  65. package/server/src/routes/beads.rs +0 -413
  66. package/server/src/routes/cli.rs +0 -150
  67. package/server/src/routes/fs.rs +0 -360
  68. package/server/src/routes/git.rs +0 -169
  69. package/server/src/routes/mod.rs +0 -107
  70. package/server/src/routes/projects.rs +0 -177
  71. package/server/src/routes/watch.rs +0 -211
  72. package/src/app/globals.css +0 -101
  73. package/src/app/layout.tsx +0 -36
  74. package/src/app/page.tsx +0 -348
  75. package/src/app/project/kanban-board.tsx +0 -356
  76. package/src/app/project/page.tsx +0 -18
  77. package/src/app/settings/page.tsx +0 -224
  78. package/src/components/Beams.css +0 -5
  79. package/src/components/Beams.jsx +0 -307
  80. package/src/components/Galaxy.css +0 -5
  81. package/src/components/Galaxy.jsx +0 -333
  82. package/src/components/activity-timeline.tsx +0 -172
  83. package/src/components/add-project-dialog.tsx +0 -219
  84. package/src/components/bead-card.tsx +0 -196
  85. package/src/components/bead-detail.tsx +0 -306
  86. package/src/components/color-picker.tsx +0 -101
  87. package/src/components/comment-input.tsx +0 -155
  88. package/src/components/comment-list.tsx +0 -147
  89. package/src/components/dependency-badge.tsx +0 -106
  90. package/src/components/design-doc-dialog.tsx +0 -58
  91. package/src/components/design-doc-preview.tsx +0 -97
  92. package/src/components/design-doc-viewer.tsx +0 -199
  93. package/src/components/editable-project-name.tsx +0 -178
  94. package/src/components/epic-card.tsx +0 -263
  95. package/src/components/folder-browser.tsx +0 -273
  96. package/src/components/footer.tsx +0 -27
  97. package/src/components/kanban/default.tsx +0 -184
  98. package/src/components/kanban-column.tsx +0 -167
  99. package/src/components/project-card.tsx +0 -191
  100. package/src/components/quick-filter-bar.tsx +0 -279
  101. package/src/components/scan-directory-dialog.tsx +0 -368
  102. package/src/components/status-donut.tsx +0 -197
  103. package/src/components/subtask-list.tsx +0 -128
  104. package/src/components/tag-picker.tsx +0 -252
  105. package/src/components/ui/.gitkeep +0 -0
  106. package/src/components/ui/alert-dialog.tsx +0 -141
  107. package/src/components/ui/avatar.tsx +0 -67
  108. package/src/components/ui/badge.tsx +0 -230
  109. package/src/components/ui/button.tsx +0 -433
  110. package/src/components/ui/card/index.tsx +0 -24
  111. package/src/components/ui/card/roiui-card.module.css +0 -197
  112. package/src/components/ui/card/roiui-card.tsx +0 -154
  113. package/src/components/ui/card/shadcn-card.tsx +0 -76
  114. package/src/components/ui/chart.tsx +0 -369
  115. package/src/components/ui/dialog.tsx +0 -122
  116. package/src/components/ui/dropdown-menu.tsx +0 -201
  117. package/src/components/ui/input.tsx +0 -22
  118. package/src/components/ui/kanban.tsx +0 -522
  119. package/src/components/ui/morphing-dialog.tsx +0 -457
  120. package/src/components/ui/popover.tsx +0 -33
  121. package/src/components/ui/progress.tsx +0 -28
  122. package/src/components/ui/scroll-area.tsx +0 -48
  123. package/src/components/ui/select.tsx +0 -159
  124. package/src/components/ui/separator.tsx +0 -31
  125. package/src/components/ui/sheet.tsx +0 -142
  126. package/src/components/ui/skeleton.tsx +0 -15
  127. package/src/components/ui/toast.tsx +0 -129
  128. package/src/components/ui/toaster.tsx +0 -35
  129. package/src/components/ui/tooltip.tsx +0 -30
  130. package/src/hooks/.gitkeep +0 -0
  131. package/src/hooks/use-bead-filters.ts +0 -261
  132. package/src/hooks/use-beads.ts +0 -162
  133. package/src/hooks/use-branch-statuses.ts +0 -161
  134. package/src/hooks/use-epics.ts +0 -173
  135. package/src/hooks/use-file-watcher.ts +0 -111
  136. package/src/hooks/use-keyboard-navigation.ts +0 -282
  137. package/src/hooks/use-project.ts +0 -61
  138. package/src/hooks/use-projects.ts +0 -93
  139. package/src/hooks/use-toast.ts +0 -194
  140. package/src/hooks/useClickOutside.tsx +0 -26
  141. package/src/lib/.gitkeep +0 -0
  142. package/src/lib/api.ts +0 -186
  143. package/src/lib/beads-parser.ts +0 -252
  144. package/src/lib/cli.ts +0 -193
  145. package/src/lib/db.ts +0 -145
  146. package/src/lib/design-doc.ts +0 -74
  147. package/src/lib/epic-parser.ts +0 -242
  148. package/src/lib/git.ts +0 -102
  149. package/src/lib/utils.ts +0 -12
  150. package/src/types/index.ts +0 -107
  151. package/tailwind.config.ts +0 -85
  152. package/tsconfig.json +0 -26
  153. /package/{npm/bin → bin}/cli.js +0 -0
  154. /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
- }