beads-kanban-ui 0.1.0 → 0.1.2

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,306 +0,0 @@
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
- }
@@ -1,101 +0,0 @@
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
- }
@@ -1,155 +0,0 @@
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
- }