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,273 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useCallback, useRef } from "react";
|
|
4
|
-
import * as api from "@/lib/api";
|
|
5
|
-
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
6
|
-
import { Button } from "@/components/ui/button";
|
|
7
|
-
import { Badge } from "@/components/ui/badge";
|
|
8
|
-
import { cn } from "@/lib/utils";
|
|
9
|
-
import { Folder, FolderOpen, ChevronRight, Home } from "lucide-react";
|
|
10
|
-
import type { FsEntry } from "@/lib/api";
|
|
11
|
-
|
|
12
|
-
interface FolderBrowserProps {
|
|
13
|
-
currentPath: string;
|
|
14
|
-
onPathChange: (path: string) => void;
|
|
15
|
-
onSelectPath: (path: string, hasBeads: boolean) => void;
|
|
16
|
-
className?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface DirectoryEntry extends FsEntry {
|
|
20
|
-
hasBeads: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function FolderBrowser({
|
|
24
|
-
currentPath,
|
|
25
|
-
onPathChange,
|
|
26
|
-
onSelectPath,
|
|
27
|
-
className,
|
|
28
|
-
}: FolderBrowserProps) {
|
|
29
|
-
const [directories, setDirectories] = useState<DirectoryEntry[]>([]);
|
|
30
|
-
const [loading, setLoading] = useState(false);
|
|
31
|
-
const [error, setError] = useState<string | null>(null);
|
|
32
|
-
const [selectedIndex, setSelectedIndex] = useState<number>(-1);
|
|
33
|
-
const [currentPathHasBeads, setCurrentPathHasBeads] = useState(false);
|
|
34
|
-
const listRef = useRef<HTMLDivElement>(null);
|
|
35
|
-
|
|
36
|
-
// Load directories when path changes
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
const loadDirectories = async () => {
|
|
39
|
-
if (!currentPath) return;
|
|
40
|
-
|
|
41
|
-
setLoading(true);
|
|
42
|
-
setError(null);
|
|
43
|
-
setSelectedIndex(-1);
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
// Fetch directory contents and check if current path has beads in parallel
|
|
47
|
-
const [listResult, currentBeadsResult] = await Promise.all([
|
|
48
|
-
api.fs.list(currentPath),
|
|
49
|
-
api.fs.exists(`${currentPath.replace(/\/+$/, "")}/.beads`),
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
// Filter to only directories
|
|
53
|
-
const dirs = listResult.entries.filter((entry) => entry.isDirectory);
|
|
54
|
-
|
|
55
|
-
// Check which directories have .beads folders in parallel
|
|
56
|
-
const dirsWithBeadsStatus = await Promise.all(
|
|
57
|
-
dirs.map(async (dir) => {
|
|
58
|
-
const beadsPath = `${dir.path}/.beads`;
|
|
59
|
-
const result = await api.fs.exists(beadsPath);
|
|
60
|
-
return {
|
|
61
|
-
...dir,
|
|
62
|
-
hasBeads: result.exists,
|
|
63
|
-
};
|
|
64
|
-
})
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// Sort: directories with .beads first, then alphabetically
|
|
68
|
-
dirsWithBeadsStatus.sort((a, b) => {
|
|
69
|
-
if (a.hasBeads && !b.hasBeads) return -1;
|
|
70
|
-
if (!a.hasBeads && b.hasBeads) return 1;
|
|
71
|
-
return a.name.localeCompare(b.name);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
setDirectories(dirsWithBeadsStatus);
|
|
75
|
-
setCurrentPathHasBeads(currentBeadsResult.exists);
|
|
76
|
-
} catch (err) {
|
|
77
|
-
console.error("Error loading directories:", err);
|
|
78
|
-
setError(err instanceof Error ? err.message : "Failed to load directories");
|
|
79
|
-
setDirectories([]);
|
|
80
|
-
} finally {
|
|
81
|
-
setLoading(false);
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
loadDirectories();
|
|
86
|
-
}, [currentPath]);
|
|
87
|
-
|
|
88
|
-
const navigateToDirectory = useCallback(
|
|
89
|
-
(path: string) => {
|
|
90
|
-
onPathChange(path);
|
|
91
|
-
},
|
|
92
|
-
[onPathChange]
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const navigateUp = useCallback(() => {
|
|
96
|
-
const parentPath = currentPath.replace(/\/[^/]+\/?$/, "") || "/";
|
|
97
|
-
onPathChange(parentPath);
|
|
98
|
-
}, [currentPath, onPathChange]);
|
|
99
|
-
|
|
100
|
-
const navigateToHome = useCallback(() => {
|
|
101
|
-
// Get home directory - on macOS/Linux it's typically /Users/username or /home/username
|
|
102
|
-
const homePath =
|
|
103
|
-
typeof window !== "undefined"
|
|
104
|
-
? "/Users" // Start at /Users on macOS for navigation
|
|
105
|
-
: "/";
|
|
106
|
-
onPathChange(homePath);
|
|
107
|
-
}, [onPathChange]);
|
|
108
|
-
|
|
109
|
-
// Build breadcrumb segments from current path
|
|
110
|
-
const pathSegments = currentPath.split("/").filter(Boolean);
|
|
111
|
-
|
|
112
|
-
const handleKeyDown = useCallback(
|
|
113
|
-
(e: React.KeyboardEvent) => {
|
|
114
|
-
if (directories.length === 0) return;
|
|
115
|
-
|
|
116
|
-
switch (e.key) {
|
|
117
|
-
case "ArrowDown":
|
|
118
|
-
e.preventDefault();
|
|
119
|
-
setSelectedIndex((prev) =>
|
|
120
|
-
prev < directories.length - 1 ? prev + 1 : prev
|
|
121
|
-
);
|
|
122
|
-
break;
|
|
123
|
-
case "ArrowUp":
|
|
124
|
-
e.preventDefault();
|
|
125
|
-
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
|
|
126
|
-
break;
|
|
127
|
-
case "Enter":
|
|
128
|
-
e.preventDefault();
|
|
129
|
-
if (selectedIndex >= 0 && selectedIndex < directories.length) {
|
|
130
|
-
navigateToDirectory(directories[selectedIndex].path);
|
|
131
|
-
}
|
|
132
|
-
break;
|
|
133
|
-
case "Backspace":
|
|
134
|
-
if (currentPath !== "/") {
|
|
135
|
-
e.preventDefault();
|
|
136
|
-
navigateUp();
|
|
137
|
-
}
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
},
|
|
141
|
-
[directories, selectedIndex, navigateToDirectory, navigateUp, currentPath]
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
const handleSelect = useCallback(() => {
|
|
145
|
-
onSelectPath(currentPath, currentPathHasBeads);
|
|
146
|
-
}, [currentPath, currentPathHasBeads, onSelectPath]);
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<div
|
|
150
|
-
className={cn("flex flex-col gap-3", className)}
|
|
151
|
-
onKeyDown={handleKeyDown}
|
|
152
|
-
tabIndex={0}
|
|
153
|
-
role="application"
|
|
154
|
-
aria-label="Folder browser"
|
|
155
|
-
>
|
|
156
|
-
{/* Breadcrumb navigation */}
|
|
157
|
-
<div className="flex items-center gap-1 rounded-md border border-zinc-700 bg-zinc-800/50 px-2 py-1.5 text-sm">
|
|
158
|
-
<Button
|
|
159
|
-
variant="ghost"
|
|
160
|
-
size="xs"
|
|
161
|
-
mode="icon"
|
|
162
|
-
onClick={navigateToHome}
|
|
163
|
-
aria-label="Go to home directory"
|
|
164
|
-
className="shrink-0"
|
|
165
|
-
>
|
|
166
|
-
<Home />
|
|
167
|
-
</Button>
|
|
168
|
-
<ChevronRight className="size-3 shrink-0 text-zinc-500" />
|
|
169
|
-
{pathSegments.map((segment, index) => {
|
|
170
|
-
const segmentPath = "/" + pathSegments.slice(0, index + 1).join("/");
|
|
171
|
-
const isLast = index === pathSegments.length - 1;
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<div key={segmentPath} className="flex items-center gap-1">
|
|
175
|
-
<button
|
|
176
|
-
type="button"
|
|
177
|
-
onClick={() => navigateToDirectory(segmentPath)}
|
|
178
|
-
className={cn(
|
|
179
|
-
"rounded px-1 py-0.5 text-sm transition-colors hover:bg-zinc-700",
|
|
180
|
-
isLast ? "text-zinc-100" : "text-zinc-400"
|
|
181
|
-
)}
|
|
182
|
-
>
|
|
183
|
-
{segment}
|
|
184
|
-
</button>
|
|
185
|
-
{!isLast && (
|
|
186
|
-
<ChevronRight className="size-3 shrink-0 text-zinc-500" />
|
|
187
|
-
)}
|
|
188
|
-
</div>
|
|
189
|
-
);
|
|
190
|
-
})}
|
|
191
|
-
</div>
|
|
192
|
-
|
|
193
|
-
{/* Current path beads indicator */}
|
|
194
|
-
{currentPathHasBeads && (
|
|
195
|
-
<div className="flex items-center gap-2 rounded-md border border-purple-500/30 bg-purple-500/10 px-3 py-2">
|
|
196
|
-
<Badge variant="info" size="sm">
|
|
197
|
-
.beads found
|
|
198
|
-
</Badge>
|
|
199
|
-
<span className="text-xs text-zinc-400">
|
|
200
|
-
This folder contains a beads project
|
|
201
|
-
</span>
|
|
202
|
-
</div>
|
|
203
|
-
)}
|
|
204
|
-
|
|
205
|
-
{/* Directory list */}
|
|
206
|
-
<ScrollArea className="h-[300px] rounded-md border border-zinc-700 bg-zinc-800/50">
|
|
207
|
-
<div ref={listRef} className="p-2" role="listbox" aria-label="Directories">
|
|
208
|
-
{loading ? (
|
|
209
|
-
<div className="flex items-center justify-center py-8 text-zinc-500">
|
|
210
|
-
<div className="size-4 animate-spin rounded-full border-2 border-zinc-500 border-t-transparent" />
|
|
211
|
-
<span className="ml-2 text-sm">Loading...</span>
|
|
212
|
-
</div>
|
|
213
|
-
) : error ? (
|
|
214
|
-
<div className="py-8 text-center text-sm text-red-400">{error}</div>
|
|
215
|
-
) : directories.length === 0 ? (
|
|
216
|
-
<div className="py-8 text-center text-sm text-zinc-500">
|
|
217
|
-
No subdirectories found
|
|
218
|
-
</div>
|
|
219
|
-
) : (
|
|
220
|
-
directories.map((dir, index) => (
|
|
221
|
-
<button
|
|
222
|
-
key={dir.path}
|
|
223
|
-
type="button"
|
|
224
|
-
role="option"
|
|
225
|
-
aria-selected={selectedIndex === index}
|
|
226
|
-
onClick={() => setSelectedIndex(index)}
|
|
227
|
-
onDoubleClick={() => navigateToDirectory(dir.path)}
|
|
228
|
-
className={cn(
|
|
229
|
-
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors",
|
|
230
|
-
selectedIndex === index
|
|
231
|
-
? "bg-zinc-700 text-zinc-100"
|
|
232
|
-
: "text-zinc-300 hover:bg-zinc-700/50",
|
|
233
|
-
dir.hasBeads && "border-l-2 border-purple-500"
|
|
234
|
-
)}
|
|
235
|
-
>
|
|
236
|
-
{selectedIndex === index ? (
|
|
237
|
-
<FolderOpen className="size-4 shrink-0 text-zinc-400" />
|
|
238
|
-
) : (
|
|
239
|
-
<Folder
|
|
240
|
-
className={cn(
|
|
241
|
-
"size-4 shrink-0",
|
|
242
|
-
dir.hasBeads ? "text-purple-400" : "text-zinc-400"
|
|
243
|
-
)}
|
|
244
|
-
/>
|
|
245
|
-
)}
|
|
246
|
-
<span className="truncate">{dir.name}</span>
|
|
247
|
-
{dir.hasBeads && (
|
|
248
|
-
<Badge variant="info" size="xs" className="ml-auto shrink-0">
|
|
249
|
-
.beads
|
|
250
|
-
</Badge>
|
|
251
|
-
)}
|
|
252
|
-
</button>
|
|
253
|
-
))
|
|
254
|
-
)}
|
|
255
|
-
</div>
|
|
256
|
-
</ScrollArea>
|
|
257
|
-
|
|
258
|
-
{/* Keyboard hints */}
|
|
259
|
-
<div className="text-xs text-zinc-500">
|
|
260
|
-
Double-click or press Enter to open. Backspace to go up.
|
|
261
|
-
</div>
|
|
262
|
-
|
|
263
|
-
{/* Select button */}
|
|
264
|
-
<Button
|
|
265
|
-
onClick={handleSelect}
|
|
266
|
-
disabled={!currentPath || loading}
|
|
267
|
-
className="w-full"
|
|
268
|
-
>
|
|
269
|
-
Select This Folder
|
|
270
|
-
</Button>
|
|
271
|
-
</div>
|
|
272
|
-
);
|
|
273
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export function Footer() {
|
|
2
|
-
return (
|
|
3
|
-
<footer className="border-t border-zinc-200 px-6 py-4">
|
|
4
|
-
<div className="flex flex-col items-center justify-center gap-2 text-sm text-zinc-500 sm:flex-row sm:gap-4">
|
|
5
|
-
<span>Beads Kanban UI</span>
|
|
6
|
-
<span className="hidden sm:inline">·</span>
|
|
7
|
-
<a
|
|
8
|
-
href="https://github.com/AvivK5498/beads-kanban-ui"
|
|
9
|
-
target="_blank"
|
|
10
|
-
rel="noopener noreferrer"
|
|
11
|
-
className="flex items-center gap-1.5 hover:text-zinc-900 transition-colors"
|
|
12
|
-
>
|
|
13
|
-
<svg
|
|
14
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
15
|
-
width="16"
|
|
16
|
-
height="16"
|
|
17
|
-
viewBox="0 0 24 24"
|
|
18
|
-
fill="currentColor"
|
|
19
|
-
>
|
|
20
|
-
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
|
21
|
-
</svg>
|
|
22
|
-
View on GitHub
|
|
23
|
-
</a>
|
|
24
|
-
</div>
|
|
25
|
-
</footer>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import * as React from 'react';
|
|
4
|
-
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
5
|
-
import { Badge } from '@/components/ui/badge';
|
|
6
|
-
import { Button } from '@/components/ui/button';
|
|
7
|
-
import {
|
|
8
|
-
Kanban,
|
|
9
|
-
KanbanBoard,
|
|
10
|
-
KanbanColumn,
|
|
11
|
-
KanbanColumnContent,
|
|
12
|
-
KanbanColumnHandle,
|
|
13
|
-
KanbanItem,
|
|
14
|
-
KanbanItemHandle,
|
|
15
|
-
KanbanOverlay,
|
|
16
|
-
} from '@/components/ui/kanban';
|
|
17
|
-
import { GripVertical } from 'lucide-react';
|
|
18
|
-
|
|
19
|
-
interface Task {
|
|
20
|
-
id: string;
|
|
21
|
-
title: string;
|
|
22
|
-
priority: 'low' | 'medium' | 'high';
|
|
23
|
-
description?: string;
|
|
24
|
-
assignee?: string;
|
|
25
|
-
assigneeAvatar?: string;
|
|
26
|
-
dueDate?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const COLUMN_TITLES: Record<string, string> = {
|
|
30
|
-
backlog: 'Backlog',
|
|
31
|
-
inProgress: 'In Progress',
|
|
32
|
-
review: 'Review',
|
|
33
|
-
done: 'Done',
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
interface TaskCardProps extends Omit<React.ComponentProps<typeof KanbanItem>, 'value' | 'children'> {
|
|
37
|
-
task: Task;
|
|
38
|
-
asHandle?: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function TaskCard({ task, asHandle, ...props }: TaskCardProps) {
|
|
42
|
-
const cardContent = (
|
|
43
|
-
<div className="rounded-md border bg-card p-3 shadow-xs">
|
|
44
|
-
<div className="flex flex-col gap-2.5">
|
|
45
|
-
<div className="flex items-center justify-between gap-2">
|
|
46
|
-
<span className="line-clamp-1 font-medium text-sm">{task.title}</span>
|
|
47
|
-
<Badge
|
|
48
|
-
variant={task.priority === 'high' ? 'destructive' : task.priority === 'medium' ? 'primary' : 'warning'}
|
|
49
|
-
appearance="outline"
|
|
50
|
-
className="pointer-events-none h-5 rounded-sm px-1.5 text-[11px] capitalize shrink-0"
|
|
51
|
-
>
|
|
52
|
-
{task.priority}
|
|
53
|
-
</Badge>
|
|
54
|
-
</div>
|
|
55
|
-
<div className="flex items-center justify-between text-muted-foreground text-xs">
|
|
56
|
-
{task.assignee && (
|
|
57
|
-
<div className="flex items-center gap-1">
|
|
58
|
-
<Avatar className="size-4">
|
|
59
|
-
<AvatarImage src={task.assigneeAvatar} />
|
|
60
|
-
<AvatarFallback>{task.assignee.charAt(0)}</AvatarFallback>
|
|
61
|
-
</Avatar>
|
|
62
|
-
<span className="line-clamp-1">{task.assignee}</span>
|
|
63
|
-
</div>
|
|
64
|
-
)}
|
|
65
|
-
{task.dueDate && <time className="text-[10px] tabular-nums whitespace-nowrap">{task.dueDate}</time>}
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<KanbanItem value={task.id} {...props}>
|
|
73
|
-
{asHandle ? <KanbanItemHandle>{cardContent}</KanbanItemHandle> : cardContent}
|
|
74
|
-
</KanbanItem>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
interface TaskColumnProps extends Omit<React.ComponentProps<typeof KanbanColumn>, 'children'> {
|
|
79
|
-
tasks: Task[];
|
|
80
|
-
isOverlay?: boolean;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function TaskColumn({ value, tasks, isOverlay, ...props }: TaskColumnProps) {
|
|
84
|
-
return (
|
|
85
|
-
<KanbanColumn value={value} {...props} className="rounded-md border bg-card p-2.5 shadow-xs">
|
|
86
|
-
<div className="flex items-center justify-between mb-2.5">
|
|
87
|
-
<div className="flex items-center gap-2.5">
|
|
88
|
-
<span className="font-semibold text-sm">{COLUMN_TITLES[value]}</span>
|
|
89
|
-
<Badge variant="secondary">{tasks.length}</Badge>
|
|
90
|
-
</div>
|
|
91
|
-
<KanbanColumnHandle asChild>
|
|
92
|
-
<Button variant="dim" size="sm" mode="icon">
|
|
93
|
-
<GripVertical />
|
|
94
|
-
</Button>
|
|
95
|
-
</KanbanColumnHandle>
|
|
96
|
-
</div>
|
|
97
|
-
<KanbanColumnContent value={value} className="flex flex-col gap-2.5 p-0.5">
|
|
98
|
-
{tasks.map((task) => (
|
|
99
|
-
<TaskCard key={task.id} task={task} asHandle={!isOverlay} />
|
|
100
|
-
))}
|
|
101
|
-
</KanbanColumnContent>
|
|
102
|
-
</KanbanColumn>
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export default function Component() {
|
|
107
|
-
const [columns, setColumns] = React.useState<Record<string, Task[]>>({
|
|
108
|
-
backlog: [
|
|
109
|
-
{
|
|
110
|
-
id: '1',
|
|
111
|
-
title: 'Add authentication',
|
|
112
|
-
priority: 'high',
|
|
113
|
-
assignee: 'John Doe',
|
|
114
|
-
assigneeAvatar: 'https://randomuser.me/api/portraits/men/1.jpg',
|
|
115
|
-
dueDate: 'Jan 10, 2025',
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
id: '2',
|
|
119
|
-
title: 'Create API endpoints',
|
|
120
|
-
priority: 'medium',
|
|
121
|
-
assignee: 'Jane Smith',
|
|
122
|
-
assigneeAvatar: 'https://randomuser.me/api/portraits/women/2.jpg',
|
|
123
|
-
dueDate: 'Jan 15, 2025',
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
id: '3',
|
|
127
|
-
title: 'Write documentation',
|
|
128
|
-
priority: 'low',
|
|
129
|
-
assignee: 'Bob Johnson',
|
|
130
|
-
assigneeAvatar: 'https://randomuser.me/api/portraits/men/3.jpg',
|
|
131
|
-
dueDate: 'Jan 20, 2025',
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
inProgress: [
|
|
135
|
-
{
|
|
136
|
-
id: '4',
|
|
137
|
-
title: 'Design system updates',
|
|
138
|
-
priority: 'high',
|
|
139
|
-
assignee: 'Alice Brown',
|
|
140
|
-
assigneeAvatar: 'https://randomuser.me/api/portraits/women/4.jpg',
|
|
141
|
-
dueDate: 'Aug 25, 2025',
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
id: '5',
|
|
145
|
-
title: 'Implement dark mode',
|
|
146
|
-
priority: 'medium',
|
|
147
|
-
assignee: 'Charlie Wilson',
|
|
148
|
-
assigneeAvatar: 'https://randomuser.me/api/portraits/men/5.jpg',
|
|
149
|
-
dueDate: 'Aug 25, 2025',
|
|
150
|
-
},
|
|
151
|
-
],
|
|
152
|
-
done: [
|
|
153
|
-
{
|
|
154
|
-
id: '7',
|
|
155
|
-
title: 'Setup project',
|
|
156
|
-
priority: 'high',
|
|
157
|
-
assignee: 'Eve Davis',
|
|
158
|
-
assigneeAvatar: 'https://randomuser.me/api/portraits/women/6.jpg',
|
|
159
|
-
dueDate: 'Sep 25, 2025',
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
id: '8',
|
|
163
|
-
title: 'Initial commit',
|
|
164
|
-
priority: 'low',
|
|
165
|
-
assignee: 'Frank White',
|
|
166
|
-
assigneeAvatar: 'https://randomuser.me/api/portraits/men/7.jpg',
|
|
167
|
-
dueDate: 'Sep 20, 2025',
|
|
168
|
-
},
|
|
169
|
-
],
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
return (
|
|
173
|
-
<Kanban value={columns} onValueChange={setColumns} getItemValue={(item) => item.id}>
|
|
174
|
-
<KanbanBoard className="grid auto-rows-fr grid-cols-3">
|
|
175
|
-
{Object.entries(columns).map(([columnValue, tasks]) => (
|
|
176
|
-
<TaskColumn key={columnValue} value={columnValue} tasks={tasks} />
|
|
177
|
-
))}
|
|
178
|
-
</KanbanBoard>
|
|
179
|
-
<KanbanOverlay>
|
|
180
|
-
<div className="rounded-md bg-muted/60 size-full" />
|
|
181
|
-
</KanbanOverlay>
|
|
182
|
-
</Kanban>
|
|
183
|
-
);
|
|
184
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
4
|
-
import { Badge } from "@/components/ui/badge";
|
|
5
|
-
import { BeadCard } from "@/components/bead-card";
|
|
6
|
-
import { EpicCard } from "@/components/epic-card";
|
|
7
|
-
import { cn } from "@/lib/utils";
|
|
8
|
-
import type { Bead, BeadStatus, Epic } from "@/types";
|
|
9
|
-
import type { BranchStatus } from "@/lib/git";
|
|
10
|
-
|
|
11
|
-
export interface KanbanColumnProps {
|
|
12
|
-
status: BeadStatus;
|
|
13
|
-
title: string;
|
|
14
|
-
beads: Bead[];
|
|
15
|
-
/** All beads for resolving epic children */
|
|
16
|
-
allBeads: Bead[];
|
|
17
|
-
selectedBeadId?: string | null;
|
|
18
|
-
ticketNumbers?: Map<string, number>;
|
|
19
|
-
branchStatuses?: Record<string, BranchStatus>;
|
|
20
|
-
onSelectBead: (bead: Bead) => void;
|
|
21
|
-
onChildClick?: (child: Bead) => void;
|
|
22
|
-
onNavigateToDependency?: (beadId: string) => void;
|
|
23
|
-
/** Project root path for fetching design docs */
|
|
24
|
-
projectPath?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Get accent border class for column header based on status
|
|
29
|
-
*/
|
|
30
|
-
function getColumnAccentBorder(status: BeadStatus): string {
|
|
31
|
-
switch (status) {
|
|
32
|
-
case "open":
|
|
33
|
-
return "border-t-2 border-t-blue-500/60";
|
|
34
|
-
case "in_progress":
|
|
35
|
-
return "border-t-2 border-t-amber-500/60";
|
|
36
|
-
case "inreview":
|
|
37
|
-
return "border-t-2 border-t-purple-500/60";
|
|
38
|
-
case "closed":
|
|
39
|
-
return "border-t-2 border-t-green-500/60";
|
|
40
|
-
default:
|
|
41
|
-
return "border-t-2 border-t-zinc-500/60";
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Get header text color based on status
|
|
47
|
-
*/
|
|
48
|
-
function getHeaderTextColor(status: BeadStatus): string {
|
|
49
|
-
switch (status) {
|
|
50
|
-
case "open":
|
|
51
|
-
return "text-blue-400";
|
|
52
|
-
case "in_progress":
|
|
53
|
-
return "text-amber-400";
|
|
54
|
-
case "inreview":
|
|
55
|
-
return "text-purple-400";
|
|
56
|
-
case "closed":
|
|
57
|
-
return "text-green-400";
|
|
58
|
-
default:
|
|
59
|
-
return "text-zinc-400";
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Get badge color class for count badge based on status (dark theme)
|
|
65
|
-
*/
|
|
66
|
-
function getBadgeVariant(status: BeadStatus): string {
|
|
67
|
-
switch (status) {
|
|
68
|
-
case "open":
|
|
69
|
-
return "bg-blue-500/20 text-blue-400 border-blue-500/30 hover:bg-blue-500/20";
|
|
70
|
-
case "in_progress":
|
|
71
|
-
return "bg-amber-500/20 text-amber-400 border-amber-500/30 hover:bg-amber-500/20";
|
|
72
|
-
case "inreview":
|
|
73
|
-
return "bg-purple-500/20 text-purple-400 border-purple-500/30 hover:bg-purple-500/20";
|
|
74
|
-
case "closed":
|
|
75
|
-
return "bg-green-500/20 text-green-400 border-green-500/30 hover:bg-green-500/20";
|
|
76
|
-
default:
|
|
77
|
-
return "bg-zinc-500/20 text-zinc-400 border-zinc-500/30 hover:bg-zinc-500/20";
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Type guard to check if a bead is an epic
|
|
83
|
-
*/
|
|
84
|
-
function isEpic(bead: Bead): bead is Epic {
|
|
85
|
-
return bead.issue_type === 'epic';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Reusable Kanban column component with header, count badge, and scrollable bead list
|
|
90
|
-
* Renders EpicCard for epics and BeadCard for standalone tasks
|
|
91
|
-
*/
|
|
92
|
-
export function KanbanColumn({
|
|
93
|
-
status,
|
|
94
|
-
title,
|
|
95
|
-
beads,
|
|
96
|
-
allBeads,
|
|
97
|
-
selectedBeadId,
|
|
98
|
-
ticketNumbers,
|
|
99
|
-
branchStatuses = {},
|
|
100
|
-
onSelectBead,
|
|
101
|
-
onChildClick,
|
|
102
|
-
onNavigateToDependency,
|
|
103
|
-
projectPath,
|
|
104
|
-
}: KanbanColumnProps) {
|
|
105
|
-
return (
|
|
106
|
-
<div
|
|
107
|
-
className={cn(
|
|
108
|
-
"flex flex-col h-full min-h-0 rounded-lg",
|
|
109
|
-
"bg-zinc-900/30 border border-zinc-800/50"
|
|
110
|
-
)}
|
|
111
|
-
>
|
|
112
|
-
{/* Column Header - fixed height with colored accent border */}
|
|
113
|
-
<div className={cn(
|
|
114
|
-
"flex-shrink-0 flex items-center justify-between px-4 py-3 border-b border-zinc-800/50",
|
|
115
|
-
getColumnAccentBorder(status)
|
|
116
|
-
)}>
|
|
117
|
-
<h2 className={cn("font-semibold text-sm", getHeaderTextColor(status))}>{title}</h2>
|
|
118
|
-
<Badge
|
|
119
|
-
variant="secondary"
|
|
120
|
-
className={cn("text-xs px-2 py-0.5", getBadgeVariant(status))}
|
|
121
|
-
>
|
|
122
|
-
{beads.length}
|
|
123
|
-
</Badge>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
{/* Scrollable Bead List */}
|
|
127
|
-
<ScrollArea className="flex-1 min-h-0 p-3">
|
|
128
|
-
<div className="space-y-3">
|
|
129
|
-
{beads.map((bead) => {
|
|
130
|
-
// Render EpicCard for epics, BeadCard for standalone tasks
|
|
131
|
-
if (isEpic(bead)) {
|
|
132
|
-
return (
|
|
133
|
-
<EpicCard
|
|
134
|
-
key={bead.id}
|
|
135
|
-
epic={bead}
|
|
136
|
-
allBeads={allBeads}
|
|
137
|
-
ticketNumber={ticketNumbers?.get(bead.id)}
|
|
138
|
-
isSelected={selectedBeadId === bead.id}
|
|
139
|
-
onSelect={onSelectBead}
|
|
140
|
-
onChildClick={onChildClick ?? onSelectBead}
|
|
141
|
-
onNavigateToDependency={onNavigateToDependency}
|
|
142
|
-
projectPath={projectPath}
|
|
143
|
-
/>
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return (
|
|
148
|
-
<BeadCard
|
|
149
|
-
key={bead.id}
|
|
150
|
-
bead={bead}
|
|
151
|
-
ticketNumber={ticketNumbers?.get(bead.id)}
|
|
152
|
-
isSelected={selectedBeadId === bead.id}
|
|
153
|
-
branchStatus={branchStatuses[bead.id]}
|
|
154
|
-
onSelect={onSelectBead}
|
|
155
|
-
/>
|
|
156
|
-
);
|
|
157
|
-
})}
|
|
158
|
-
{beads.length === 0 && (
|
|
159
|
-
<div className="text-center py-8 text-zinc-500 text-sm">
|
|
160
|
-
No beads
|
|
161
|
-
</div>
|
|
162
|
-
)}
|
|
163
|
-
</div>
|
|
164
|
-
</ScrollArea>
|
|
165
|
-
</div>
|
|
166
|
-
);
|
|
167
|
-
}
|