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,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
- }