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,147 +0,0 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
- import { formatDistanceToNow } from "date-fns";
5
- import { cn } from "@/lib/utils";
6
- import { Button } from "@/components/ui/button";
7
- import { Input } from "@/components/ui/input";
8
- import { beads as beadsApi } from "@/lib/api";
9
- import type { Comment } from "@/types";
10
-
11
- export interface CommentListProps {
12
- comments: Comment[];
13
- beadId: string;
14
- projectPath: string;
15
- onCommentAdded?: () => void;
16
- }
17
-
18
- /**
19
- * Format a date string to relative time (e.g., "2 hours ago")
20
- */
21
- function formatRelativeTime(dateString: string): string {
22
- try {
23
- const date = new Date(dateString);
24
- return formatDistanceToNow(date, { addSuffix: true });
25
- } catch {
26
- return dateString;
27
- }
28
- }
29
-
30
- /**
31
- * Single comment card component
32
- */
33
- function CommentCard({ comment }: { comment: Comment }) {
34
- return (
35
- <div
36
- className={cn(
37
- "rounded-lg border border-zinc-800 bg-zinc-900/50 p-3 space-y-1.5"
38
- )}
39
- >
40
- {/* Author and timestamp */}
41
- <div className="flex items-center gap-2 text-sm">
42
- <span className="font-semibold text-zinc-200">{comment.author}</span>
43
- <span className="text-zinc-500">
44
- {formatRelativeTime(comment.created_at)}
45
- </span>
46
- </div>
47
-
48
- {/* Comment text */}
49
- <p className="text-sm text-zinc-300 leading-relaxed whitespace-pre-wrap">
50
- {comment.text}
51
- </p>
52
- </div>
53
- );
54
- }
55
-
56
- /**
57
- * Comments list component displaying all comments for a bead
58
- */
59
- export function CommentList({
60
- comments,
61
- beadId,
62
- projectPath,
63
- onCommentAdded,
64
- }: CommentListProps) {
65
- const [newComment, setNewComment] = useState("");
66
- const [isSubmitting, setIsSubmitting] = useState(false);
67
- const [error, setError] = useState<string | null>(null);
68
-
69
- const handleAddComment = async () => {
70
- if (!newComment.trim()) return;
71
-
72
- setIsSubmitting(true);
73
- setError(null);
74
-
75
- try {
76
- // Get author from localStorage or use default
77
- const author = localStorage.getItem("beads-author") || "user";
78
- await beadsApi.addComment(projectPath, beadId, newComment.trim(), author);
79
- setNewComment("");
80
- onCommentAdded?.();
81
- } catch (err) {
82
- setError(err instanceof Error ? err.message : "Failed to add comment");
83
- } finally {
84
- setIsSubmitting(false);
85
- }
86
- };
87
-
88
- const handleKeyDown = (e: React.KeyboardEvent) => {
89
- if (e.key === "Enter" && !e.shiftKey) {
90
- e.preventDefault();
91
- handleAddComment();
92
- }
93
- };
94
-
95
- return (
96
- <div className="space-y-4">
97
- {/* Header */}
98
- <div className="flex items-center gap-2">
99
- <h3 className="font-semibold text-sm">
100
- Comments ({comments.length})
101
- </h3>
102
- </div>
103
-
104
- {/* Divider */}
105
- <div className="border-t" />
106
-
107
- {/* Comments or empty state */}
108
- {comments.length === 0 ? (
109
- <p className="text-sm text-zinc-500 py-4 text-center">
110
- No comments yet
111
- </p>
112
- ) : (
113
- <div className="space-y-3">
114
- {comments.map((comment) => (
115
- <CommentCard key={comment.id} comment={comment} />
116
- ))}
117
- </div>
118
- )}
119
-
120
- {/* Add Comment Form */}
121
- <div className="pt-2 space-y-2">
122
- <div className="flex gap-2">
123
- <Input
124
- type="text"
125
- placeholder="Add a comment…"
126
- aria-label="Add a comment"
127
- value={newComment}
128
- onChange={(e) => setNewComment(e.target.value)}
129
- onKeyDown={handleKeyDown}
130
- disabled={isSubmitting}
131
- className="flex-1"
132
- />
133
- <Button
134
- onClick={handleAddComment}
135
- disabled={isSubmitting || !newComment.trim()}
136
- size="sm"
137
- >
138
- {isSubmitting ? "Adding…" : "Add"}
139
- </Button>
140
- </div>
141
- {error && (
142
- <p className="text-sm text-destructive">{error}</p>
143
- )}
144
- </div>
145
- </div>
146
- );
147
- }
@@ -1,106 +0,0 @@
1
- "use client";
2
-
3
- import { Badge } from "@/components/ui/badge";
4
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
5
- import { cn } from "@/lib/utils";
6
- import { AlertCircle, Lock } from "lucide-react";
7
-
8
- export interface DependencyBadgeProps {
9
- /** Bead IDs that this task depends on (blockers) */
10
- deps?: string[];
11
- /** Bead IDs that depend on this task (this task blocks them) */
12
- blockers?: string[];
13
- /** Callback when clicking on a dependency to navigate */
14
- onNavigate?: (beadId: string) => void;
15
- }
16
-
17
- /**
18
- * Shows blocked/blocking status with tooltip
19
- * Red badge if this task is blocked (has unresolved deps)
20
- * Orange badge if this task blocks others
21
- */
22
- export function DependencyBadge({ deps, blockers, onNavigate }: DependencyBadgeProps) {
23
- // Handle null values from data (default params only work for undefined)
24
- const safeDeps = deps ?? [];
25
- const safeBlockers = blockers ?? [];
26
- const isBlocked = safeDeps.length > 0;
27
- const isBlocking = safeBlockers.length > 0;
28
-
29
- if (!isBlocked && !isBlocking) {
30
- return null;
31
- }
32
-
33
- // Show blocked status with priority
34
- if (isBlocked) {
35
- return (
36
- <TooltipProvider>
37
- <Tooltip>
38
- <TooltipTrigger asChild>
39
- <Badge
40
- variant="destructive"
41
- className="text-[10px] px-1.5 py-0 cursor-help"
42
- >
43
- <Lock className="h-3 w-3 mr-0.5" aria-hidden="true" />
44
- BLOCKED
45
- </Badge>
46
- </TooltipTrigger>
47
- <TooltipContent side="top" className="max-w-xs">
48
- <div className="space-y-1">
49
- <p className="font-semibold">Blocked by:</p>
50
- {safeDeps.map((depId) => (
51
- <button
52
- key={depId}
53
- onClick={(e) => {
54
- e.stopPropagation();
55
- onNavigate?.(depId);
56
- }}
57
- aria-label={`Navigate to blocker ${depId}`}
58
- className="block text-left hover:underline w-full"
59
- >
60
- {depId}
61
- </button>
62
- ))}
63
- </div>
64
- </TooltipContent>
65
- </Tooltip>
66
- </TooltipProvider>
67
- );
68
- }
69
-
70
- // Show blocking status
71
- return (
72
- <TooltipProvider>
73
- <Tooltip>
74
- <TooltipTrigger asChild>
75
- <Badge
76
- className={cn(
77
- "text-[10px] px-1.5 py-0 cursor-help",
78
- "bg-orange-500 text-white hover:bg-orange-500/80 border-transparent"
79
- )}
80
- >
81
- <AlertCircle className="h-3 w-3 mr-0.5" aria-hidden="true" />
82
- BLOCKING
83
- </Badge>
84
- </TooltipTrigger>
85
- <TooltipContent side="top" className="max-w-xs">
86
- <div className="space-y-1">
87
- <p className="font-semibold">Blocking:</p>
88
- {safeBlockers.map((blockerId) => (
89
- <button
90
- key={blockerId}
91
- onClick={(e) => {
92
- e.stopPropagation();
93
- onNavigate?.(blockerId);
94
- }}
95
- aria-label={`Navigate to blocked task ${blockerId}`}
96
- className="block text-left hover:underline w-full"
97
- >
98
- {blockerId}
99
- </button>
100
- ))}
101
- </div>
102
- </TooltipContent>
103
- </Tooltip>
104
- </TooltipProvider>
105
- );
106
- }
@@ -1,58 +0,0 @@
1
- "use client";
2
-
3
- import ReactMarkdown from "react-markdown";
4
- import rehypeHighlight from "rehype-highlight";
5
- import {
6
- Dialog,
7
- DialogContent,
8
- DialogHeader,
9
- DialogTitle,
10
- } from "@/components/ui/dialog";
11
- import { ScrollArea } from "@/components/ui/scroll-area";
12
- import { designDocProseClasses } from "@/lib/design-doc";
13
- import { FileText } from "lucide-react";
14
- import "highlight.js/styles/github-dark.css";
15
-
16
- export interface DesignDocDialogProps {
17
- /** Whether dialog is open */
18
- open: boolean;
19
- /** Callback when dialog state changes */
20
- onOpenChange: (open: boolean) => void;
21
- /** Markdown content to display */
22
- content: string;
23
- /** Epic ID for display in title */
24
- epicId: string;
25
- }
26
-
27
- /**
28
- * Full-screen design document dialog with markdown rendering
29
- * Shows complete design doc at 60% viewport width with syntax highlighting
30
- */
31
- export function DesignDocDialog({
32
- open,
33
- onOpenChange,
34
- content,
35
- epicId,
36
- }: DesignDocDialogProps) {
37
- return (
38
- <Dialog open={open} onOpenChange={onOpenChange}>
39
- <DialogContent className="w-[60vw] max-w-[60vw] max-h-[80vh] p-0 bg-zinc-900 border-zinc-800">
40
- <DialogHeader className="px-6 pt-6 pb-3 border-b border-zinc-800">
41
- <div className="flex items-center gap-2">
42
- <FileText className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
43
- <DialogTitle className="text-base font-semibold">
44
- Design Document - {epicId}
45
- </DialogTitle>
46
- </div>
47
- </DialogHeader>
48
- <ScrollArea className="h-[calc(80vh-5rem)]">
49
- <div className={`p-6 ${designDocProseClasses}`}>
50
- <ReactMarkdown rehypePlugins={[rehypeHighlight]}>
51
- {content}
52
- </ReactMarkdown>
53
- </div>
54
- </ScrollArea>
55
- </DialogContent>
56
- </Dialog>
57
- );
58
- }
@@ -1,97 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useEffect } from "react";
4
- import { Button } from "@/components/ui/button";
5
- import { fetchDesignDoc, truncateMarkdownToPlainText } from "@/lib/design-doc";
6
- import { DesignDocDialog } from "@/components/design-doc-dialog";
7
- import { Loader2, FileText, AlertCircle } from "lucide-react";
8
-
9
- export interface DesignDocPreviewProps {
10
- /** Path to design doc (e.g., ".designs/BD-001.md") */
11
- designDocPath: string;
12
- /** Epic ID for display */
13
- epicId: string;
14
- /** Project root path (absolute) */
15
- projectPath: string;
16
- }
17
-
18
- /**
19
- * Design doc preview component with collapsible preview and full document dialog
20
- * Shows truncated plain text preview (~180 chars) with "View Full Document" button
21
- */
22
- export function DesignDocPreview({
23
- designDocPath,
24
- epicId,
25
- projectPath,
26
- }: DesignDocPreviewProps) {
27
- const [content, setContent] = useState<string>("");
28
- const [isLoading, setIsLoading] = useState(true);
29
- const [error, setError] = useState<string | null>(null);
30
- const [isDialogOpen, setIsDialogOpen] = useState(false);
31
-
32
- useEffect(() => {
33
- const loadDoc = async () => {
34
- try {
35
- setIsLoading(true);
36
- setError(null);
37
- const docContent = await fetchDesignDoc(designDocPath, projectPath);
38
- setContent(docContent);
39
- } catch (err) {
40
- setError(err instanceof Error ? err.message : "Failed to load design doc");
41
- } finally {
42
- setIsLoading(false);
43
- }
44
- };
45
-
46
- void loadDoc();
47
- }, [designDocPath, projectPath]);
48
-
49
- if (isLoading) {
50
- return (
51
- <div className="flex items-center gap-2 text-muted-foreground text-xs py-2">
52
- <Loader2 className="h-3 w-3 animate-spin" aria-hidden="true" />
53
- <span>Loading design document…</span>
54
- </div>
55
- );
56
- }
57
-
58
- if (error) {
59
- return (
60
- <div className="flex items-center gap-2 text-destructive text-xs py-2">
61
- <AlertCircle className="h-3 w-3" aria-hidden="true" />
62
- <span>{error}</span>
63
- </div>
64
- );
65
- }
66
-
67
- const preview = truncateMarkdownToPlainText(content, 180);
68
-
69
- return (
70
- <>
71
- <div className="space-y-2">
72
- <p className="text-xs text-muted-foreground leading-relaxed">
73
- {preview}
74
- </p>
75
- <Button
76
- variant="outline"
77
- size="sm"
78
- onClick={(e) => {
79
- e.stopPropagation();
80
- setIsDialogOpen(true);
81
- }}
82
- className="text-xs h-7"
83
- >
84
- <FileText className="h-3 w-3 mr-1.5" aria-hidden="true" />
85
- View Full Document
86
- </Button>
87
- </div>
88
-
89
- <DesignDocDialog
90
- open={isDialogOpen}
91
- onOpenChange={setIsDialogOpen}
92
- content={content}
93
- epicId={epicId}
94
- />
95
- </>
96
- );
97
- }
@@ -1,199 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useEffect, useCallback } from "react";
4
- import ReactMarkdown from "react-markdown";
5
- import rehypeHighlight from "rehype-highlight";
6
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
- import { Badge } from "@/components/ui/badge";
8
- import { ScrollArea } from "@/components/ui/scroll-area";
9
- import { cn } from "@/lib/utils";
10
- import { FileText, Loader2 } from "lucide-react";
11
- import {
12
- MorphingDialog,
13
- MorphingDialogTrigger,
14
- MorphingDialogContent,
15
- MorphingDialogContainer,
16
- MorphingDialogClose,
17
- MorphingDialogTitle,
18
- MorphingDialogDescription,
19
- } from "@/components/ui/morphing-dialog";
20
- import "highlight.js/styles/github-dark.css";
21
-
22
- const API_BASE = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3008';
23
-
24
- export interface DesignDocViewerProps {
25
- /** Path to design doc (e.g., ".designs/{EPIC_ID}.md") */
26
- designDocPath: string;
27
- /** Epic ID for display */
28
- epicId: string;
29
- /** Project root path (absolute) */
30
- projectPath: string;
31
- /** Callback when fullscreen state changes */
32
- onFullScreenChange?: (isFullScreen: boolean) => void;
33
- /** Whether the dialog should start in open state */
34
- defaultOpen?: boolean;
35
- }
36
-
37
- /**
38
- * Fetch design doc content from API
39
- */
40
- async function fetchDesignDoc(path: string, projectPath: string): Promise<string> {
41
- const encodedPath = encodeURIComponent(path);
42
- const encodedProjectPath = encodeURIComponent(projectPath);
43
- const response = await fetch(
44
- `${API_BASE}/api/fs/read?path=${encodedPath}&project_path=${encodedProjectPath}`
45
- );
46
- if (!response.ok) {
47
- throw new Error('Failed to fetch design doc: ' + response.statusText);
48
- }
49
- const data = await response.json();
50
- return data.content || '';
51
- }
52
-
53
- /** Prose styles for markdown rendering */
54
- const proseStyles = cn(
55
- "prose prose-sm dark:prose-invert max-w-none",
56
- "prose-headings:scroll-mt-20",
57
- "prose-pre:bg-zinc-900 prose-pre:text-zinc-100",
58
- "prose-code:text-sm prose-code:bg-zinc-100 dark:prose-code:bg-zinc-800",
59
- "prose-code:px-1 prose-code:py-0.5 prose-code:rounded"
60
- );
61
-
62
- /**
63
- * Markdown renderer for design docs with syntax highlighting
64
- * Uses MorphingDialog for smooth expand/collapse animation
65
- */
66
- export function DesignDocViewer({ designDocPath, epicId, projectPath, onFullScreenChange, defaultOpen }: DesignDocViewerProps) {
67
- const [content, setContent] = useState<string>("");
68
- const [isLoading, setIsLoading] = useState(true);
69
- const [error, setError] = useState<string | null>(null);
70
-
71
- const handleOpenChange = useCallback((isOpen: boolean) => {
72
- onFullScreenChange?.(isOpen);
73
- }, [onFullScreenChange]);
74
-
75
- useEffect(() => {
76
- const loadDoc = async () => {
77
- try {
78
- setIsLoading(true);
79
- setError(null);
80
- const docContent = await fetchDesignDoc(designDocPath, projectPath);
81
- setContent(docContent);
82
- } catch (err) {
83
- setError(err instanceof Error ? err.message : "Failed to load design doc");
84
- } finally {
85
- setIsLoading(false);
86
- }
87
- };
88
-
89
- loadDoc();
90
- }, [designDocPath, projectPath]);
91
-
92
- if (isLoading) {
93
- return (
94
- <Card>
95
- <CardContent className="p-6">
96
- <div className="flex items-center justify-center gap-2 text-muted-foreground">
97
- <Loader2 className="size-4 animate-spin" aria-hidden="true" />
98
- <span className="text-sm">Loading design document…</span>
99
- </div>
100
- </CardContent>
101
- </Card>
102
- );
103
- }
104
-
105
- if (error) {
106
- return (
107
- <Card>
108
- <CardContent className="p-6">
109
- <div className="text-sm text-destructive">
110
- <p className="font-semibold">Error loading design document</p>
111
- <p className="text-xs mt-1">{error}</p>
112
- </div>
113
- </CardContent>
114
- </Card>
115
- );
116
- }
117
-
118
- // Extract first heading or first line as preview
119
- const firstLine = content.split('\n').find(line => line.trim()) || 'Design Document';
120
- const previewText = firstLine.replace(/^#+\s*/, '').slice(0, 100);
121
-
122
- return (
123
- <MorphingDialog
124
- transition={{
125
- type: 'spring',
126
- stiffness: 200,
127
- damping: 24,
128
- }}
129
- onOpenChange={handleOpenChange}
130
- defaultOpen={defaultOpen}
131
- >
132
- <MorphingDialogTrigger className="w-full text-left">
133
- <Card className="cursor-pointer hover:bg-accent/50 transition-colors">
134
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
135
- <div className="flex items-center gap-2">
136
- <FileText className="size-4 text-muted-foreground" aria-hidden="true" />
137
- <MorphingDialogTitle>
138
- <CardTitle className="text-sm font-semibold">Design Document</CardTitle>
139
- </MorphingDialogTitle>
140
- <Badge variant="outline" className="text-[10px] px-1.5 py-0">
141
- {epicId}
142
- </Badge>
143
- </div>
144
- </CardHeader>
145
- <CardContent className="pt-0 pb-4">
146
- <MorphingDialogDescription
147
- disableLayoutAnimation
148
- variants={{
149
- initial: { opacity: 1 },
150
- animate: { opacity: 1 },
151
- exit: { opacity: 0 },
152
- }}
153
- >
154
- <p className="text-xs text-muted-foreground line-clamp-2">
155
- {previewText}
156
- </p>
157
- </MorphingDialogDescription>
158
- </CardContent>
159
- </Card>
160
- </MorphingDialogTrigger>
161
-
162
- <MorphingDialogContainer>
163
- <MorphingDialogContent
164
- className="relative bg-zinc-900 border-zinc-800 rounded-lg shadow-lg w-[60vw] max-h-[80vh] overflow-hidden"
165
- >
166
- {/* Fixed header outside scroll area */}
167
- <div className="flex items-center gap-2 px-6 pt-6 pb-3 border-b border-zinc-800">
168
- <FileText className="size-4 text-muted-foreground" aria-hidden="true" />
169
- <MorphingDialogTitle>
170
- <h2 className="text-sm font-semibold">Design Document</h2>
171
- </MorphingDialogTitle>
172
- <Badge variant="outline" className="text-[10px] px-1.5 py-0">
173
- {epicId}
174
- </Badge>
175
- </div>
176
- {/* Scrollable content with explicit height */}
177
- <ScrollArea className="h-[calc(80vh-5rem)]">
178
- <div className="p-6">
179
- <MorphingDialogDescription
180
- disableLayoutAnimation
181
- variants={{
182
- initial: { opacity: 0, scale: 0.98 },
183
- animate: { opacity: 1, scale: 1 },
184
- exit: { opacity: 0, scale: 0.98 },
185
- }}
186
- className={proseStyles}
187
- >
188
- <ReactMarkdown rehypePlugins={[rehypeHighlight]}>
189
- {content}
190
- </ReactMarkdown>
191
- </MorphingDialogDescription>
192
- </div>
193
- </ScrollArea>
194
- <MorphingDialogClose className="absolute top-4 right-4" />
195
- </MorphingDialogContent>
196
- </MorphingDialogContainer>
197
- </MorphingDialog>
198
- );
199
- }