beads-kanban-ui 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/.designs/beads-kanban-ui-bj0.md +73 -0
- package/.designs/beads-kanban-ui-qxq.md +144 -0
- package/.designs/epic-support.md +282 -0
- package/.env.local.example +2 -0
- package/.eslintrc.json +3 -0
- package/.gitattributes +3 -0
- package/.github/workflows/release.yml +123 -0
- package/.history/README_20260121193710.md +227 -0
- package/.history/README_20260121193918.md +227 -0
- package/.history/README_20260121193921.md +227 -0
- package/.history/README_20260121193933.md +227 -0
- package/.history/README_20260121193934.md +227 -0
- package/.history/README_20260121193944.md +227 -0
- package/.history/README_20260121193953.md +227 -0
- package/.history/src/app/page_20260121133429.tsx +134 -0
- package/.history/src/app/page_20260121133928.tsx +134 -0
- package/.history/src/app/page_20260121144850.tsx +138 -0
- package/.history/src/app/page_20260121144854.tsx +138 -0
- package/.history/src/app/page_20260121144858.tsx +138 -0
- package/.history/src/app/page_20260121144902.tsx +138 -0
- package/.history/src/app/page_20260121144906.tsx +138 -0
- package/.history/src/app/page_20260121144911.tsx +138 -0
- package/.history/src/app/page_20260121144928.tsx +138 -0
- 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/README.md +243 -0
- package/Screenshots/bead-detail.png +0 -0
- package/Screenshots/dashboard.png +0 -0
- package/Screenshots/kanban-board.png +0 -0
- package/components.json +27 -0
- package/logo/logo.svg +1 -0
- package/next.config.js +9 -0
- package/npm/README.md +37 -0
- package/npm/bin/cli.js +107 -0
- package/npm/package.json +20 -0
- package/npm/scripts/postinstall.js +132 -0
- package/package.json +62 -0
- package/postcss.config.js +6 -0
- package/public/logo.svg +1 -0
- package/restart.sh +5 -0
- package/server/Cargo.lock +1685 -0
- package/server/Cargo.toml +24 -0
- package/server/src/db.rs +570 -0
- package/server/src/main.rs +141 -0
- package/server/src/routes/beads.rs +413 -0
- package/server/src/routes/cli.rs +150 -0
- package/server/src/routes/fs.rs +360 -0
- package/server/src/routes/git.rs +169 -0
- package/server/src/routes/mod.rs +107 -0
- package/server/src/routes/projects.rs +177 -0
- package/server/src/routes/watch.rs +211 -0
- package/src/app/globals.css +101 -0
- package/src/app/layout.tsx +36 -0
- package/src/app/page.tsx +348 -0
- package/src/app/project/kanban-board.tsx +356 -0
- package/src/app/project/page.tsx +18 -0
- package/src/app/settings/page.tsx +224 -0
- package/src/components/Beams.css +5 -0
- package/src/components/Beams.jsx +307 -0
- package/src/components/Galaxy.css +5 -0
- package/src/components/Galaxy.jsx +333 -0
- package/src/components/activity-timeline.tsx +172 -0
- package/src/components/add-project-dialog.tsx +219 -0
- package/src/components/bead-card.tsx +196 -0
- package/src/components/bead-detail.tsx +306 -0
- package/src/components/color-picker.tsx +101 -0
- package/src/components/comment-input.tsx +155 -0
- package/src/components/comment-list.tsx +147 -0
- package/src/components/dependency-badge.tsx +106 -0
- package/src/components/design-doc-dialog.tsx +58 -0
- package/src/components/design-doc-preview.tsx +97 -0
- package/src/components/design-doc-viewer.tsx +199 -0
- package/src/components/editable-project-name.tsx +178 -0
- package/src/components/epic-card.tsx +263 -0
- package/src/components/folder-browser.tsx +273 -0
- package/src/components/footer.tsx +27 -0
- package/src/components/kanban/default.tsx +184 -0
- package/src/components/kanban-column.tsx +167 -0
- package/src/components/project-card.tsx +191 -0
- package/src/components/quick-filter-bar.tsx +279 -0
- package/src/components/scan-directory-dialog.tsx +368 -0
- package/src/components/status-donut.tsx +197 -0
- package/src/components/subtask-list.tsx +128 -0
- package/src/components/tag-picker.tsx +252 -0
- package/src/components/ui/.gitkeep +0 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/avatar.tsx +67 -0
- package/src/components/ui/badge.tsx +230 -0
- package/src/components/ui/button.tsx +433 -0
- package/src/components/ui/card/index.tsx +24 -0
- package/src/components/ui/card/roiui-card.module.css +197 -0
- package/src/components/ui/card/roiui-card.tsx +154 -0
- package/src/components/ui/card/shadcn-card.tsx +76 -0
- package/src/components/ui/chart.tsx +369 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/kanban.tsx +522 -0
- package/src/components/ui/morphing-dialog.tsx +457 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/sheet.tsx +142 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/toast.tsx +129 -0
- package/src/components/ui/toaster.tsx +35 -0
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-bead-filters.ts +261 -0
- package/src/hooks/use-beads.ts +162 -0
- package/src/hooks/use-branch-statuses.ts +161 -0
- package/src/hooks/use-epics.ts +173 -0
- package/src/hooks/use-file-watcher.ts +111 -0
- package/src/hooks/use-keyboard-navigation.ts +282 -0
- package/src/hooks/use-project.ts +61 -0
- package/src/hooks/use-projects.ts +93 -0
- package/src/hooks/use-toast.ts +194 -0
- package/src/hooks/useClickOutside.tsx +26 -0
- package/src/lib/.gitkeep +0 -0
- package/src/lib/api.ts +186 -0
- package/src/lib/beads-parser.ts +252 -0
- package/src/lib/cli.ts +193 -0
- package/src/lib/db.ts +145 -0
- package/src/lib/design-doc.ts +74 -0
- package/src/lib/epic-parser.ts +242 -0
- package/src/lib/git.ts +102 -0
- package/src/lib/utils.ts +12 -0
- package/src/types/index.ts +107 -0
- package/tailwind.config.ts +85 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Sheet,
|
|
5
|
+
SheetContent,
|
|
6
|
+
SheetHeader,
|
|
7
|
+
SheetTitle,
|
|
8
|
+
SheetDescription,
|
|
9
|
+
} from "@/components/ui/sheet";
|
|
10
|
+
import { Badge } from "@/components/ui/badge";
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { cn } from "@/lib/utils";
|
|
13
|
+
import type { Bead, BeadStatus } from "@/types";
|
|
14
|
+
import { ArrowLeft, GitBranch, Calendar } from "lucide-react";
|
|
15
|
+
import { DesignDocViewer } from "@/components/design-doc-viewer";
|
|
16
|
+
import { SubtaskList } from "@/components/subtask-list";
|
|
17
|
+
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
18
|
+
|
|
19
|
+
export interface BeadDetailProps {
|
|
20
|
+
bead: Bead;
|
|
21
|
+
ticketNumber?: number;
|
|
22
|
+
open: boolean;
|
|
23
|
+
onOpenChange: (open: boolean) => void;
|
|
24
|
+
children?: React.ReactNode;
|
|
25
|
+
/** Project root path (absolute) */
|
|
26
|
+
projectPath?: string;
|
|
27
|
+
/** All beads for resolving child task IDs */
|
|
28
|
+
allBeads?: Bead[];
|
|
29
|
+
/** Callback when clicking a child task */
|
|
30
|
+
onChildClick?: (child: Bead) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get status badge color classes based on status
|
|
35
|
+
*/
|
|
36
|
+
function getStatusColor(status: BeadStatus): string {
|
|
37
|
+
switch (status) {
|
|
38
|
+
case "open":
|
|
39
|
+
return "bg-blue-500/20 text-blue-400 border-blue-500/30";
|
|
40
|
+
case "in_progress":
|
|
41
|
+
return "bg-amber-500/20 text-amber-400 border-amber-500/30";
|
|
42
|
+
case "inreview":
|
|
43
|
+
return "bg-purple-500/20 text-purple-400 border-purple-500/30";
|
|
44
|
+
case "closed":
|
|
45
|
+
return "bg-green-500/20 text-green-400 border-green-500/30";
|
|
46
|
+
default:
|
|
47
|
+
return "bg-zinc-500/20 text-zinc-400 border-zinc-500/30";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get priority badge color classes based on priority level
|
|
53
|
+
*/
|
|
54
|
+
function getPriorityColor(priority: number): string {
|
|
55
|
+
switch (priority) {
|
|
56
|
+
case 0:
|
|
57
|
+
return "bg-red-500/20 text-red-400 border-red-500/30";
|
|
58
|
+
case 1:
|
|
59
|
+
return "bg-orange-500/20 text-orange-400 border-orange-500/30";
|
|
60
|
+
case 2:
|
|
61
|
+
return "bg-zinc-500/20 text-zinc-400 border-zinc-500/30";
|
|
62
|
+
default:
|
|
63
|
+
return "bg-zinc-600/20 text-zinc-500 border-zinc-600/30";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format status for display (e.g., "in_progress" -> "In Progress")
|
|
69
|
+
*/
|
|
70
|
+
function formatStatus(status: BeadStatus): string {
|
|
71
|
+
switch (status) {
|
|
72
|
+
case "open":
|
|
73
|
+
return "Open";
|
|
74
|
+
case "in_progress":
|
|
75
|
+
return "In Progress";
|
|
76
|
+
case "inreview":
|
|
77
|
+
return "In Review";
|
|
78
|
+
case "closed":
|
|
79
|
+
return "Closed";
|
|
80
|
+
default:
|
|
81
|
+
return status;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Format bead ID for display (uppercase BD prefix)
|
|
87
|
+
*/
|
|
88
|
+
function formatBeadId(id: string): string {
|
|
89
|
+
if (id.startsWith("BD-") || id.startsWith("bd-")) {
|
|
90
|
+
return id.toUpperCase();
|
|
91
|
+
}
|
|
92
|
+
const parts = id.split("-");
|
|
93
|
+
const shortId = parts[parts.length - 1];
|
|
94
|
+
return `BD-${shortId.slice(0, 8).toUpperCase()}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Format date for display
|
|
99
|
+
*/
|
|
100
|
+
function formatDate(dateString: string): string {
|
|
101
|
+
try {
|
|
102
|
+
const date = new Date(dateString);
|
|
103
|
+
return date.toLocaleDateString("en-US", {
|
|
104
|
+
month: "short",
|
|
105
|
+
day: "numeric",
|
|
106
|
+
year: date.getFullYear() !== new Date().getFullYear() ? "numeric" : undefined,
|
|
107
|
+
});
|
|
108
|
+
} catch {
|
|
109
|
+
return dateString;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Bead detail sheet component - slides in from the right
|
|
115
|
+
* Displays full bead information with metadata grid and description
|
|
116
|
+
*
|
|
117
|
+
* Note: When DesignDocViewer goes fullscreen, we hide the Sheet via CSS and
|
|
118
|
+
* override Radix's scroll lock to allow the MorphingDialog to function properly.
|
|
119
|
+
* This avoids the conflict between Radix Dialog and MorphingDialog scroll locks.
|
|
120
|
+
*/
|
|
121
|
+
export function BeadDetail({
|
|
122
|
+
bead,
|
|
123
|
+
ticketNumber,
|
|
124
|
+
open,
|
|
125
|
+
onOpenChange,
|
|
126
|
+
children,
|
|
127
|
+
projectPath,
|
|
128
|
+
allBeads,
|
|
129
|
+
onChildClick,
|
|
130
|
+
}: BeadDetailProps) {
|
|
131
|
+
const branchName = `bd-${formatBeadId(bead.id)}`;
|
|
132
|
+
const [isDesignDocFullScreen, setIsDesignDocFullScreen] = useState(false);
|
|
133
|
+
const hasDesignDoc = !!bead.design_doc;
|
|
134
|
+
|
|
135
|
+
// Check if this is an epic with children
|
|
136
|
+
const isEpic = bead.children && bead.children.length > 0;
|
|
137
|
+
|
|
138
|
+
// Resolve children from IDs
|
|
139
|
+
const childTasks = useMemo(() => {
|
|
140
|
+
if (!isEpic || !allBeads) return [];
|
|
141
|
+
return (bead.children || [])
|
|
142
|
+
.map(childId => allBeads.find(b => b.id === childId))
|
|
143
|
+
.filter((b): b is Bead => b !== undefined);
|
|
144
|
+
}, [isEpic, bead.children, allBeads]);
|
|
145
|
+
|
|
146
|
+
// Handle fullscreen state changes from DesignDocViewer
|
|
147
|
+
const handleFullScreenChange = useCallback((isFullScreen: boolean) => {
|
|
148
|
+
setIsDesignDocFullScreen(isFullScreen);
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
// Override Radix's scroll lock when MorphingDialog is fullscreen
|
|
152
|
+
// This fixes the pointer-events: none issue on body
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (isDesignDocFullScreen) {
|
|
155
|
+
// Remove Radix's scroll lock styles that conflict with MorphingDialog
|
|
156
|
+
document.body.style.pointerEvents = '';
|
|
157
|
+
document.body.style.overflow = 'hidden'; // MorphingDialog will manage this
|
|
158
|
+
}
|
|
159
|
+
}, [isDesignDocFullScreen]);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<>
|
|
163
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
164
|
+
<SheetContent
|
|
165
|
+
side="right"
|
|
166
|
+
className={cn(
|
|
167
|
+
"w-full sm:max-w-lg md:max-w-xl overflow-y-auto bg-[#0a0a0a] border-zinc-800",
|
|
168
|
+
isDesignDocFullScreen && "invisible"
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
{/* Header with Back button */}
|
|
172
|
+
<div className="flex items-center justify-between mb-6">
|
|
173
|
+
<Button
|
|
174
|
+
variant="ghost"
|
|
175
|
+
size="sm"
|
|
176
|
+
onClick={() => onOpenChange(false)}
|
|
177
|
+
className="gap-1.5 -ml-2"
|
|
178
|
+
>
|
|
179
|
+
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
|
180
|
+
Back
|
|
181
|
+
</Button>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<SheetHeader className="space-y-4">
|
|
185
|
+
{/* Ticket Number + Bead ID */}
|
|
186
|
+
<SheetDescription className="text-xs font-mono text-zinc-500">
|
|
187
|
+
{ticketNumber !== undefined && (
|
|
188
|
+
<span className="font-semibold text-zinc-200">#{ticketNumber}</span>
|
|
189
|
+
)}
|
|
190
|
+
{ticketNumber !== undefined && " "}
|
|
191
|
+
{formatBeadId(bead.id)}
|
|
192
|
+
</SheetDescription>
|
|
193
|
+
|
|
194
|
+
{/* Title */}
|
|
195
|
+
<SheetTitle className="text-xl font-semibold leading-tight text-zinc-100">
|
|
196
|
+
{bead.title}
|
|
197
|
+
</SheetTitle>
|
|
198
|
+
</SheetHeader>
|
|
199
|
+
|
|
200
|
+
{/* Metadata Grid */}
|
|
201
|
+
<div className="mt-6 rounded-lg border border-zinc-800 bg-zinc-900/50 p-4">
|
|
202
|
+
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
203
|
+
{/* Status */}
|
|
204
|
+
<div className="space-y-1">
|
|
205
|
+
<span className="text-zinc-500 text-xs">Status</span>
|
|
206
|
+
<div>
|
|
207
|
+
<Badge
|
|
208
|
+
variant="outline"
|
|
209
|
+
className={cn("font-medium", getStatusColor(bead.status))}
|
|
210
|
+
>
|
|
211
|
+
{formatStatus(bead.status)}
|
|
212
|
+
</Badge>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
{/* Priority */}
|
|
217
|
+
<div className="space-y-1">
|
|
218
|
+
<span className="text-zinc-500 text-xs">Priority</span>
|
|
219
|
+
<div>
|
|
220
|
+
<Badge
|
|
221
|
+
variant="outline"
|
|
222
|
+
className={cn("font-medium", getPriorityColor(bead.priority))}
|
|
223
|
+
>
|
|
224
|
+
P{bead.priority}
|
|
225
|
+
</Badge>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Type */}
|
|
230
|
+
<div className="space-y-1">
|
|
231
|
+
<span className="text-zinc-500 text-xs">Type</span>
|
|
232
|
+
<div>
|
|
233
|
+
<Badge variant="outline" className="font-normal capitalize text-zinc-200 border-zinc-700">
|
|
234
|
+
{bead.issue_type}
|
|
235
|
+
</Badge>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{/* Branch */}
|
|
240
|
+
<div className="space-y-1">
|
|
241
|
+
<span className="text-zinc-500 text-xs">Branch</span>
|
|
242
|
+
<div className="flex items-center gap-1.5">
|
|
243
|
+
<GitBranch className="h-3.5 w-3.5 text-zinc-500" aria-hidden="true" />
|
|
244
|
+
<span className="font-mono text-xs text-zinc-200">{branchName}</span>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{/* Created */}
|
|
249
|
+
<div className="space-y-1">
|
|
250
|
+
<span className="text-zinc-500 text-xs">Created</span>
|
|
251
|
+
<div className="flex items-center gap-1.5">
|
|
252
|
+
<Calendar className="h-3.5 w-3.5 text-zinc-500" aria-hidden="true" />
|
|
253
|
+
<span className="text-xs text-zinc-200">{formatDate(bead.created_at)}</span>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{/* Description */}
|
|
260
|
+
{bead.description && (
|
|
261
|
+
<div className="mt-6">
|
|
262
|
+
<h3 className="text-sm font-semibold mb-2 text-zinc-200">Description</h3>
|
|
263
|
+
<div className="h-px bg-zinc-800 mb-3" />
|
|
264
|
+
<div className="text-sm text-zinc-400 leading-relaxed whitespace-pre-wrap">
|
|
265
|
+
{bead.description}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
|
|
270
|
+
{/* Subtasks (for epics) */}
|
|
271
|
+
{isEpic && onChildClick && (
|
|
272
|
+
<div className="mt-6">
|
|
273
|
+
<h3 className="text-sm font-semibold mb-2 text-zinc-200">
|
|
274
|
+
Subtasks ({childTasks.length})
|
|
275
|
+
</h3>
|
|
276
|
+
<div className="h-px bg-zinc-800 mb-3" />
|
|
277
|
+
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-3">
|
|
278
|
+
<SubtaskList
|
|
279
|
+
childTasks={childTasks}
|
|
280
|
+
onChildClick={onChildClick}
|
|
281
|
+
isExpanded={true}
|
|
282
|
+
/>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
{/* Design Document */}
|
|
288
|
+
{hasDesignDoc && projectPath && (
|
|
289
|
+
<div className="mt-6">
|
|
290
|
+
<h3 className="text-sm font-semibold mb-3 text-zinc-200">Design Document</h3>
|
|
291
|
+
<DesignDocViewer
|
|
292
|
+
designDocPath={bead.design_doc!}
|
|
293
|
+
epicId={formatBeadId(bead.id)}
|
|
294
|
+
projectPath={projectPath}
|
|
295
|
+
onFullScreenChange={handleFullScreenChange}
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
|
|
300
|
+
{/* Children slot for comments + timeline */}
|
|
301
|
+
{children && <div className="mt-6">{children}</div>}
|
|
302
|
+
</SheetContent>
|
|
303
|
+
</Sheet>
|
|
304
|
+
</>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Check } from "lucide-react";
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Input } from "@/components/ui/input";
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
|
|
10
|
+
const PRESET_COLORS = [
|
|
11
|
+
"#ef4444", // red
|
|
12
|
+
"#f97316", // orange
|
|
13
|
+
"#eab308", // yellow
|
|
14
|
+
"#22c55e", // green
|
|
15
|
+
"#14b8a6", // teal
|
|
16
|
+
"#3b82f6", // blue
|
|
17
|
+
"#8b5cf6", // violet
|
|
18
|
+
"#ec4899", // pink
|
|
19
|
+
"#6b7280", // gray
|
|
20
|
+
"#78716c", // stone
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
interface ColorPickerProps {
|
|
24
|
+
value: string;
|
|
25
|
+
onChange: (color: string) => void;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ColorPicker({ value, onChange, className }: ColorPickerProps) {
|
|
30
|
+
const [customColor, setCustomColor] = React.useState(value);
|
|
31
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
32
|
+
|
|
33
|
+
const handleCustomColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
34
|
+
const newColor = e.target.value;
|
|
35
|
+
setCustomColor(newColor);
|
|
36
|
+
// Only update if it's a valid hex color
|
|
37
|
+
if (/^#[0-9A-Fa-f]{6}$/.test(newColor)) {
|
|
38
|
+
onChange(newColor);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handlePresetClick = (color: string) => {
|
|
43
|
+
onChange(color);
|
|
44
|
+
setCustomColor(color);
|
|
45
|
+
setIsOpen(false);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
50
|
+
<PopoverTrigger asChild>
|
|
51
|
+
<Button
|
|
52
|
+
variant="outline"
|
|
53
|
+
size="sm"
|
|
54
|
+
className={cn("h-8 w-8 p-0 border-2", className)}
|
|
55
|
+
style={{ backgroundColor: value }}
|
|
56
|
+
>
|
|
57
|
+
<span className="sr-only">Pick a color</span>
|
|
58
|
+
</Button>
|
|
59
|
+
</PopoverTrigger>
|
|
60
|
+
<PopoverContent className="w-auto p-3" align="start">
|
|
61
|
+
<div className="space-y-3">
|
|
62
|
+
{/* Preset colors */}
|
|
63
|
+
<div className="grid grid-cols-5 gap-2">
|
|
64
|
+
{PRESET_COLORS.map((color) => (
|
|
65
|
+
<button
|
|
66
|
+
key={color}
|
|
67
|
+
className={cn(
|
|
68
|
+
"h-7 w-7 rounded-md border-2 transition-transform hover:scale-110",
|
|
69
|
+
value === color ? "border-zinc-900 ring-2 ring-zinc-900 ring-offset-1" : "border-transparent"
|
|
70
|
+
)}
|
|
71
|
+
style={{ backgroundColor: color }}
|
|
72
|
+
onClick={() => handlePresetClick(color)}
|
|
73
|
+
type="button"
|
|
74
|
+
>
|
|
75
|
+
{value === color && (
|
|
76
|
+
<Check className="h-4 w-4 mx-auto text-white drop-shadow-sm" />
|
|
77
|
+
)}
|
|
78
|
+
<span className="sr-only">{color}</span>
|
|
79
|
+
</button>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Custom hex input */}
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
<div
|
|
86
|
+
className="h-7 w-7 rounded-md border"
|
|
87
|
+
style={{ backgroundColor: customColor }}
|
|
88
|
+
/>
|
|
89
|
+
<Input
|
|
90
|
+
value={customColor}
|
|
91
|
+
onChange={handleCustomColorChange}
|
|
92
|
+
placeholder="#000000"
|
|
93
|
+
className="h-8 font-mono text-sm"
|
|
94
|
+
maxLength={7}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</PopoverContent>
|
|
99
|
+
</Popover>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { addComment } from "@/lib/cli";
|
|
7
|
+
import { toast } from "@/hooks/use-toast";
|
|
8
|
+
import { Loader2, CornerDownLeft } from "lucide-react";
|
|
9
|
+
|
|
10
|
+
export interface CommentInputProps {
|
|
11
|
+
/** The ID of the bead to add comments to */
|
|
12
|
+
beadId: string;
|
|
13
|
+
/** Optional project path for CLI command execution */
|
|
14
|
+
projectPath?: string;
|
|
15
|
+
/** Callback fired after a comment is successfully added */
|
|
16
|
+
onCommentAdded?: () => void;
|
|
17
|
+
/** Additional CSS classes */
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* CommentInput component for adding comments to beads via CLI
|
|
23
|
+
*
|
|
24
|
+
* Features:
|
|
25
|
+
* - Textarea with placeholder
|
|
26
|
+
* - Submit on Enter (Cmd/Ctrl+Enter for newline)
|
|
27
|
+
* - Loading state while submitting
|
|
28
|
+
* - Success/error toasts
|
|
29
|
+
* - Clears input on success
|
|
30
|
+
*/
|
|
31
|
+
export function CommentInput({
|
|
32
|
+
beadId,
|
|
33
|
+
projectPath,
|
|
34
|
+
onCommentAdded,
|
|
35
|
+
className,
|
|
36
|
+
}: CommentInputProps) {
|
|
37
|
+
const [comment, setComment] = React.useState("");
|
|
38
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
39
|
+
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
|
40
|
+
|
|
41
|
+
const handleSubmit = React.useCallback(async () => {
|
|
42
|
+
const trimmedComment = comment.trim();
|
|
43
|
+
|
|
44
|
+
if (!trimmedComment) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setIsSubmitting(true);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await addComment(beadId, trimmedComment, projectPath);
|
|
52
|
+
|
|
53
|
+
// Clear input on success
|
|
54
|
+
setComment("");
|
|
55
|
+
|
|
56
|
+
// Show success toast
|
|
57
|
+
toast({
|
|
58
|
+
title: "Comment added",
|
|
59
|
+
description: `Comment added to ${beadId}`,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Notify parent
|
|
63
|
+
onCommentAdded?.();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Show error toast with message
|
|
66
|
+
const errorMessage =
|
|
67
|
+
error instanceof Error ? error.message : "Unknown error occurred";
|
|
68
|
+
|
|
69
|
+
toast({
|
|
70
|
+
variant: "destructive",
|
|
71
|
+
title: "Failed to add comment",
|
|
72
|
+
description: errorMessage,
|
|
73
|
+
});
|
|
74
|
+
} finally {
|
|
75
|
+
setIsSubmitting(false);
|
|
76
|
+
// Refocus textarea after submission
|
|
77
|
+
textareaRef.current?.focus();
|
|
78
|
+
}
|
|
79
|
+
}, [beadId, comment, projectPath, onCommentAdded]);
|
|
80
|
+
|
|
81
|
+
const handleKeyDown = React.useCallback(
|
|
82
|
+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
83
|
+
// Submit on Enter (without modifier)
|
|
84
|
+
// Allow Cmd/Ctrl+Enter for newline
|
|
85
|
+
if (e.key === "Enter" && !e.metaKey && !e.ctrlKey && !e.shiftKey) {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
handleSubmit();
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
[handleSubmit]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const handleChange = React.useCallback(
|
|
94
|
+
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
95
|
+
setComment(e.target.value);
|
|
96
|
+
},
|
|
97
|
+
[]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const canSubmit = comment.trim().length > 0 && !isSubmitting;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className={cn("relative", className)}>
|
|
104
|
+
<textarea
|
|
105
|
+
ref={textareaRef}
|
|
106
|
+
value={comment}
|
|
107
|
+
onChange={handleChange}
|
|
108
|
+
onKeyDown={handleKeyDown}
|
|
109
|
+
placeholder="Add a comment..."
|
|
110
|
+
disabled={isSubmitting}
|
|
111
|
+
rows={1}
|
|
112
|
+
className={cn(
|
|
113
|
+
"flex w-full resize-none rounded-md border border-input bg-transparent",
|
|
114
|
+
"px-3 py-2 pr-12 text-sm shadow-sm transition-colors",
|
|
115
|
+
"placeholder:text-muted-foreground",
|
|
116
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
117
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
118
|
+
"min-h-[40px] max-h-[120px]"
|
|
119
|
+
)}
|
|
120
|
+
style={{
|
|
121
|
+
// Auto-resize based on content
|
|
122
|
+
height: "auto",
|
|
123
|
+
minHeight: "40px",
|
|
124
|
+
}}
|
|
125
|
+
onInput={(e) => {
|
|
126
|
+
// Auto-resize textarea
|
|
127
|
+
const target = e.target as HTMLTextAreaElement;
|
|
128
|
+
target.style.height = "auto";
|
|
129
|
+
target.style.height = `${Math.min(target.scrollHeight, 120)}px`;
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
132
|
+
|
|
133
|
+
{/* Submit button */}
|
|
134
|
+
<Button
|
|
135
|
+
type="button"
|
|
136
|
+
size="icon"
|
|
137
|
+
variant="ghost"
|
|
138
|
+
onClick={handleSubmit}
|
|
139
|
+
disabled={!canSubmit}
|
|
140
|
+
className={cn(
|
|
141
|
+
"absolute right-1 top-1/2 -translate-y-1/2",
|
|
142
|
+
"h-8 w-8 transition-opacity",
|
|
143
|
+
canSubmit ? "opacity-100" : "opacity-50"
|
|
144
|
+
)}
|
|
145
|
+
title="Submit comment (Enter)"
|
|
146
|
+
>
|
|
147
|
+
{isSubmitting ? (
|
|
148
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
149
|
+
) : (
|
|
150
|
+
<CornerDownLeft className="h-4 w-4" />
|
|
151
|
+
)}
|
|
152
|
+
</Button>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|