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.
- 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,356 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useMemo, useRef, useState, useCallback } from "react";
|
|
4
|
-
import Link from "next/link";
|
|
5
|
-
import { useSearchParams, useRouter } from "next/navigation";
|
|
6
|
-
import { useEffect } from "react";
|
|
7
|
-
import { ArrowLeft } from "lucide-react";
|
|
8
|
-
import { QuickFilterBar } from "@/components/quick-filter-bar";
|
|
9
|
-
import { Button } from "@/components/ui/button";
|
|
10
|
-
import { KanbanColumn } from "@/components/kanban-column";
|
|
11
|
-
import { BeadDetail } from "@/components/bead-detail";
|
|
12
|
-
import { CommentList } from "@/components/comment-list";
|
|
13
|
-
import { ActivityTimeline } from "@/components/activity-timeline";
|
|
14
|
-
import { EditableProjectName } from "@/components/editable-project-name";
|
|
15
|
-
import { useBeads } from "@/hooks/use-beads";
|
|
16
|
-
import { useProject } from "@/hooks/use-project";
|
|
17
|
-
import { useBeadFilters } from "@/hooks/use-bead-filters";
|
|
18
|
-
import { useBranchStatuses } from "@/hooks/use-branch-statuses";
|
|
19
|
-
import { useKeyboardNavigation } from "@/hooks/use-keyboard-navigation";
|
|
20
|
-
import type { Bead, BeadStatus } from "@/types";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Column configuration for the Kanban board
|
|
24
|
-
* Note: Cancelled status is hidden per requirements
|
|
25
|
-
*/
|
|
26
|
-
const COLUMNS: { status: BeadStatus; title: string }[] = [
|
|
27
|
-
{ status: "open", title: "Open" },
|
|
28
|
-
{ status: "in_progress", title: "In Progress" },
|
|
29
|
-
{ status: "inreview", title: "In Review" },
|
|
30
|
-
{ status: "closed", title: "Closed" },
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Issue type filter options
|
|
35
|
-
*/
|
|
36
|
-
type IssueTypeFilter = "all" | "epics" | "tasks";
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Main Kanban board component with 4 columns, search, filter, and keyboard navigation
|
|
40
|
-
*/
|
|
41
|
-
export default function KanbanBoard() {
|
|
42
|
-
const searchParams = useSearchParams();
|
|
43
|
-
const router = useRouter();
|
|
44
|
-
const projectId = searchParams.get('id');
|
|
45
|
-
|
|
46
|
-
// Fetch project data from SQLite
|
|
47
|
-
const {
|
|
48
|
-
project,
|
|
49
|
-
isLoading: projectLoading,
|
|
50
|
-
error: projectError,
|
|
51
|
-
refetch: refetchProject,
|
|
52
|
-
} = useProject(projectId);
|
|
53
|
-
|
|
54
|
-
// Fetch beads from project path
|
|
55
|
-
const {
|
|
56
|
-
beads,
|
|
57
|
-
ticketNumbers,
|
|
58
|
-
isLoading: beadsLoading,
|
|
59
|
-
error: beadsError,
|
|
60
|
-
refresh: refreshBeads,
|
|
61
|
-
} = useBeads(project?.path ?? "");
|
|
62
|
-
|
|
63
|
-
// Use the bead filters hook with 300ms debounce
|
|
64
|
-
const {
|
|
65
|
-
filters,
|
|
66
|
-
setFilters,
|
|
67
|
-
filteredBeads,
|
|
68
|
-
clearFilters,
|
|
69
|
-
hasActiveFilters,
|
|
70
|
-
availableOwners,
|
|
71
|
-
} = useBeadFilters(beads, ticketNumbers, 300);
|
|
72
|
-
|
|
73
|
-
// Issue type filter state (epics vs tasks)
|
|
74
|
-
const [typeFilter, setTypeFilter] = useState<IssueTypeFilter>("all");
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Toggle a status in the filter
|
|
78
|
-
*/
|
|
79
|
-
const toggleStatus = useCallback((status: BeadStatus) => {
|
|
80
|
-
const newStatuses = filters.statuses.includes(status)
|
|
81
|
-
? filters.statuses.filter(s => s !== status)
|
|
82
|
-
: [...filters.statuses, status];
|
|
83
|
-
setFilters({ statuses: newStatuses });
|
|
84
|
-
}, [filters.statuses, setFilters]);
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Toggle an owner in the filter
|
|
88
|
-
*/
|
|
89
|
-
const toggleOwner = useCallback((owner: string) => {
|
|
90
|
-
const newOwners = filters.owners.includes(owner)
|
|
91
|
-
? filters.owners.filter(o => o !== owner)
|
|
92
|
-
: [...filters.owners, owner];
|
|
93
|
-
setFilters({ owners: newOwners });
|
|
94
|
-
}, [filters.owners, setFilters]);
|
|
95
|
-
|
|
96
|
-
// Fetch branch statuses for all beads
|
|
97
|
-
const beadIds = useMemo(() => beads.map((b) => b.id), [beads]);
|
|
98
|
-
const { statuses: branchStatuses } = useBranchStatuses(
|
|
99
|
-
project?.path ?? "",
|
|
100
|
-
beadIds
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Filter to only top-level beads (no parent_id)
|
|
105
|
-
* Then apply issue type filter (epics vs tasks)
|
|
106
|
-
* Child tasks should not appear in columns - they appear inside epic cards
|
|
107
|
-
*/
|
|
108
|
-
const topLevelBeads = useMemo(() => {
|
|
109
|
-
const topLevel = filteredBeads.filter(b => !b.parent_id);
|
|
110
|
-
|
|
111
|
-
// Apply issue type filter
|
|
112
|
-
if (typeFilter === "all") return topLevel;
|
|
113
|
-
if (typeFilter === "epics") return topLevel.filter(b => b.issue_type === "epic");
|
|
114
|
-
if (typeFilter === "tasks") return topLevel.filter(b => b.issue_type !== "epic");
|
|
115
|
-
|
|
116
|
-
return topLevel;
|
|
117
|
-
}, [filteredBeads, typeFilter]);
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Group top-level beads by status for columns
|
|
121
|
-
*/
|
|
122
|
-
const filteredBeadsByStatus = useMemo(() => {
|
|
123
|
-
const grouped: Record<BeadStatus, Bead[]> = {
|
|
124
|
-
open: [],
|
|
125
|
-
in_progress: [],
|
|
126
|
-
inreview: [],
|
|
127
|
-
closed: [],
|
|
128
|
-
};
|
|
129
|
-
topLevelBeads.forEach((bead) => {
|
|
130
|
-
grouped[bead.status].push(bead);
|
|
131
|
-
});
|
|
132
|
-
return grouped;
|
|
133
|
-
}, [topLevelBeads]);
|
|
134
|
-
|
|
135
|
-
// Detail sheet state
|
|
136
|
-
const [detailBeadId, setDetailBeadId] = useState<string | null>(null);
|
|
137
|
-
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
|
138
|
-
|
|
139
|
-
// Get the actual bead from the current beads array to ensure fresh data
|
|
140
|
-
const detailBead = useMemo(() => {
|
|
141
|
-
if (!detailBeadId) return null;
|
|
142
|
-
return beads.find((b) => b.id === detailBeadId) || null;
|
|
143
|
-
}, [detailBeadId, beads]);
|
|
144
|
-
|
|
145
|
-
// Ref for search input (keyboard navigation)
|
|
146
|
-
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
147
|
-
|
|
148
|
-
// Keyboard navigation (use top-level beads for navigation)
|
|
149
|
-
const { selectedId } = useKeyboardNavigation({
|
|
150
|
-
beads: topLevelBeads,
|
|
151
|
-
beadsByStatus: filteredBeadsByStatus,
|
|
152
|
-
selectedId: null,
|
|
153
|
-
onSelect: () => {
|
|
154
|
-
// Just highlight, don't open detail
|
|
155
|
-
},
|
|
156
|
-
onOpen: (bead) => {
|
|
157
|
-
setDetailBeadId(bead.id);
|
|
158
|
-
setIsDetailOpen(true);
|
|
159
|
-
},
|
|
160
|
-
onClose: () => {
|
|
161
|
-
setIsDetailOpen(false);
|
|
162
|
-
},
|
|
163
|
-
searchInputRef,
|
|
164
|
-
isDetailOpen,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// Redirect if no project ID
|
|
168
|
-
useEffect(() => {
|
|
169
|
-
if (!projectId) {
|
|
170
|
-
router.replace("/");
|
|
171
|
-
}
|
|
172
|
-
}, [projectId, router]);
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Handle bead selection - opens detail panel
|
|
176
|
-
* Works for both epics and standalone tasks
|
|
177
|
-
*/
|
|
178
|
-
const handleSelectBead = (bead: Bead) => {
|
|
179
|
-
setDetailBeadId(bead.id);
|
|
180
|
-
setIsDetailOpen(true);
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Handle child task click from within an epic
|
|
185
|
-
*/
|
|
186
|
-
const handleChildClick = (child: Bead) => {
|
|
187
|
-
setDetailBeadId(child.id);
|
|
188
|
-
setIsDetailOpen(true);
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Handle navigation to a dependency from DependencyBadge
|
|
193
|
-
*/
|
|
194
|
-
const handleNavigateToDependency = (beadId: string) => {
|
|
195
|
-
setDetailBeadId(beadId);
|
|
196
|
-
setIsDetailOpen(true);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
// Redirect state while no project ID
|
|
200
|
-
if (!projectId) {
|
|
201
|
-
return (
|
|
202
|
-
<div className="dark flex min-h-dvh items-center justify-center bg-[#0a0a0a]">
|
|
203
|
-
<p className="text-zinc-500">Redirecting…</p>
|
|
204
|
-
</div>
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Show loading state
|
|
209
|
-
if (projectLoading) {
|
|
210
|
-
return (
|
|
211
|
-
<div className="dark flex items-center justify-center min-h-dvh bg-[#0a0a0a]">
|
|
212
|
-
<div role="status" className="text-zinc-500">Loading project…</div>
|
|
213
|
-
</div>
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Show project error state
|
|
218
|
-
if (projectError) {
|
|
219
|
-
return (
|
|
220
|
-
<div className="dark flex flex-col items-center justify-center min-h-dvh bg-[#0a0a0a] gap-4">
|
|
221
|
-
<div role="alert" className="text-red-400">Error: {projectError.message}</div>
|
|
222
|
-
<Button variant="outline" asChild>
|
|
223
|
-
<Link href="/">Back to projects</Link>
|
|
224
|
-
</Button>
|
|
225
|
-
</div>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Project not found
|
|
230
|
-
if (!project) {
|
|
231
|
-
return (
|
|
232
|
-
<div className="dark flex flex-col items-center justify-center min-h-dvh bg-[#0a0a0a] gap-4">
|
|
233
|
-
<div className="text-zinc-500">Project not found</div>
|
|
234
|
-
<Button variant="outline" asChild>
|
|
235
|
-
<Link href="/">Back to projects</Link>
|
|
236
|
-
</Button>
|
|
237
|
-
</div>
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return (
|
|
242
|
-
<div className="dark min-h-dvh bg-[#0a0a0a] flex flex-col">
|
|
243
|
-
{/* Header */}
|
|
244
|
-
<header className="sticky top-0 z-30 flex items-center justify-center border-b border-zinc-800 bg-[#0a0a0a]/80 backdrop-blur-sm px-4 py-3">
|
|
245
|
-
{/* Left: Back button - absolute positioned */}
|
|
246
|
-
<div className="absolute left-4">
|
|
247
|
-
<Button variant="ghost" size="icon" asChild>
|
|
248
|
-
<Link href="/">
|
|
249
|
-
<ArrowLeft className="h-4 w-4" />
|
|
250
|
-
<span className="sr-only">Back to projects</span>
|
|
251
|
-
</Link>
|
|
252
|
-
</Button>
|
|
253
|
-
</div>
|
|
254
|
-
|
|
255
|
-
{/* Center: Project name */}
|
|
256
|
-
<EditableProjectName
|
|
257
|
-
projectId={project.id}
|
|
258
|
-
initialName={project.name}
|
|
259
|
-
onNameUpdated={refetchProject}
|
|
260
|
-
/>
|
|
261
|
-
</header>
|
|
262
|
-
|
|
263
|
-
{/* Quick Filter Bar */}
|
|
264
|
-
<div className="px-4 py-2 border-b border-zinc-800">
|
|
265
|
-
<QuickFilterBar
|
|
266
|
-
// Search
|
|
267
|
-
search={filters.search}
|
|
268
|
-
onSearchChange={(value) => setFilters({ search: value })}
|
|
269
|
-
searchInputRef={searchInputRef}
|
|
270
|
-
// Type filter
|
|
271
|
-
typeFilter={typeFilter}
|
|
272
|
-
onTypeFilterChange={setTypeFilter}
|
|
273
|
-
// Today
|
|
274
|
-
todayOnly={filters.todayOnly}
|
|
275
|
-
onTodayOnlyChange={(value) => setFilters({ todayOnly: value })}
|
|
276
|
-
// Sort
|
|
277
|
-
sortField={filters.sortField}
|
|
278
|
-
sortDirection={filters.sortDirection}
|
|
279
|
-
onSortChange={(field, direction) => setFilters({ sortField: field, sortDirection: direction })}
|
|
280
|
-
// Status/Owner filters
|
|
281
|
-
statuses={filters.statuses}
|
|
282
|
-
onStatusToggle={toggleStatus}
|
|
283
|
-
owners={filters.owners}
|
|
284
|
-
onOwnerToggle={toggleOwner}
|
|
285
|
-
availableOwners={availableOwners}
|
|
286
|
-
onClearFilters={clearFilters}
|
|
287
|
-
hasActiveFilters={hasActiveFilters}
|
|
288
|
-
/>
|
|
289
|
-
</div>
|
|
290
|
-
|
|
291
|
-
{/* Kanban Columns */}
|
|
292
|
-
<main className="flex-1 overflow-hidden p-4">
|
|
293
|
-
{beadsLoading ? (
|
|
294
|
-
<div className="flex items-center justify-center h-full">
|
|
295
|
-
<div role="status" className="text-zinc-500">Loading beads…</div>
|
|
296
|
-
</div>
|
|
297
|
-
) : beadsError ? (
|
|
298
|
-
<div className="flex items-center justify-center h-full">
|
|
299
|
-
<div role="alert" className="text-red-400">Error loading beads: {beadsError.message}</div>
|
|
300
|
-
</div>
|
|
301
|
-
) : (
|
|
302
|
-
<div className="grid grid-cols-4 gap-4 h-full">
|
|
303
|
-
{COLUMNS.map(({ status, title }) => (
|
|
304
|
-
<KanbanColumn
|
|
305
|
-
key={status}
|
|
306
|
-
status={status}
|
|
307
|
-
title={title}
|
|
308
|
-
beads={filteredBeadsByStatus[status] || []}
|
|
309
|
-
allBeads={beads}
|
|
310
|
-
selectedBeadId={selectedId}
|
|
311
|
-
ticketNumbers={ticketNumbers}
|
|
312
|
-
branchStatuses={branchStatuses}
|
|
313
|
-
onSelectBead={handleSelectBead}
|
|
314
|
-
onChildClick={handleChildClick}
|
|
315
|
-
onNavigateToDependency={handleNavigateToDependency}
|
|
316
|
-
projectPath={project?.path}
|
|
317
|
-
/>
|
|
318
|
-
))}
|
|
319
|
-
</div>
|
|
320
|
-
)}
|
|
321
|
-
</main>
|
|
322
|
-
|
|
323
|
-
{/* Bead Detail Sheet */}
|
|
324
|
-
{detailBead && (
|
|
325
|
-
<BeadDetail
|
|
326
|
-
bead={detailBead}
|
|
327
|
-
ticketNumber={ticketNumbers.get(detailBead.id)}
|
|
328
|
-
open={isDetailOpen}
|
|
329
|
-
onOpenChange={(open) => {
|
|
330
|
-
setIsDetailOpen(open);
|
|
331
|
-
if (!open) {
|
|
332
|
-
setDetailBeadId(null);
|
|
333
|
-
}
|
|
334
|
-
}}
|
|
335
|
-
projectPath={project?.path ?? ""}
|
|
336
|
-
allBeads={beads}
|
|
337
|
-
onChildClick={handleChildClick}
|
|
338
|
-
>
|
|
339
|
-
<CommentList
|
|
340
|
-
comments={detailBead.comments}
|
|
341
|
-
beadId={detailBead.id}
|
|
342
|
-
projectPath={project?.path ?? ""}
|
|
343
|
-
onCommentAdded={refreshBeads}
|
|
344
|
-
/>
|
|
345
|
-
<ActivityTimeline
|
|
346
|
-
bead={detailBead}
|
|
347
|
-
comments={detailBead.comments}
|
|
348
|
-
childBeads={(detailBead.children || [])
|
|
349
|
-
.map(id => beads.find(b => b.id === id))
|
|
350
|
-
.filter((b): b is Bead => !!b)}
|
|
351
|
-
/>
|
|
352
|
-
</BeadDetail>
|
|
353
|
-
)}
|
|
354
|
-
</div>
|
|
355
|
-
);
|
|
356
|
-
}
|
package/src/app/project/page.tsx
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { Suspense } from 'react';
|
|
2
|
-
import KanbanBoard from './kanban-board';
|
|
3
|
-
|
|
4
|
-
function LoadingFallback() {
|
|
5
|
-
return (
|
|
6
|
-
<div className="flex items-center justify-center h-screen">
|
|
7
|
-
<div className="text-muted-foreground">Loading...</div>
|
|
8
|
-
</div>
|
|
9
|
-
);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export default function ProjectPage() {
|
|
13
|
-
return (
|
|
14
|
-
<Suspense fallback={<LoadingFallback />}>
|
|
15
|
-
<KanbanBoard />
|
|
16
|
-
</Suspense>
|
|
17
|
-
);
|
|
18
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect } from "react";
|
|
4
|
-
import Link from "next/link";
|
|
5
|
-
import { getTags, createTag, deleteTag, type Tag } from "@/lib/db";
|
|
6
|
-
import { ColorPicker } from "@/components/color-picker";
|
|
7
|
-
import { Input } from "@/components/ui/input";
|
|
8
|
-
import { Button } from "@/components/ui/button";
|
|
9
|
-
|
|
10
|
-
export default function SettingsPage() {
|
|
11
|
-
const [tags, setTags] = useState<Tag[]>([]);
|
|
12
|
-
const [isAddingTag, setIsAddingTag] = useState(false);
|
|
13
|
-
const [newTagName, setNewTagName] = useState("");
|
|
14
|
-
const [newTagColor, setNewTagColor] = useState("#3b82f6");
|
|
15
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
async function loadTags() {
|
|
19
|
-
try {
|
|
20
|
-
const loadedTags = await getTags();
|
|
21
|
-
setTags(loadedTags);
|
|
22
|
-
} catch (error) {
|
|
23
|
-
console.error("Failed to load tags:", error);
|
|
24
|
-
}
|
|
25
|
-
setIsLoading(false);
|
|
26
|
-
}
|
|
27
|
-
loadTags();
|
|
28
|
-
}, []);
|
|
29
|
-
|
|
30
|
-
const handleCreateTag = async () => {
|
|
31
|
-
if (!newTagName.trim()) return;
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const tag = await createTag({ name: newTagName.trim(), color: newTagColor });
|
|
35
|
-
setTags((prev) => [...prev, tag]);
|
|
36
|
-
setNewTagName("");
|
|
37
|
-
setNewTagColor("#3b82f6");
|
|
38
|
-
setIsAddingTag(false);
|
|
39
|
-
} catch (error) {
|
|
40
|
-
console.error("Failed to create tag:", error);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const handleDeleteTag = async (tagId: string) => {
|
|
45
|
-
try {
|
|
46
|
-
await deleteTag(tagId);
|
|
47
|
-
setTags((prev) => prev.filter((t) => t.id !== tagId));
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.error("Failed to delete tag:", error);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<div className="dark min-h-dvh bg-[#0a0a0a]">
|
|
55
|
-
{/* Header */}
|
|
56
|
-
<header className="sticky top-0 z-30 border-b border-zinc-800 bg-[#0a0a0a]/80 backdrop-blur-sm px-6 py-4">
|
|
57
|
-
<div className="flex items-center gap-4">
|
|
58
|
-
<Link
|
|
59
|
-
href="/"
|
|
60
|
-
aria-label="Go back to home"
|
|
61
|
-
className="rounded-md p-2 text-zinc-400 hover:bg-zinc-800/50 hover:text-zinc-100"
|
|
62
|
-
>
|
|
63
|
-
<svg
|
|
64
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
65
|
-
width="20"
|
|
66
|
-
height="20"
|
|
67
|
-
viewBox="0 0 24 24"
|
|
68
|
-
fill="none"
|
|
69
|
-
stroke="currentColor"
|
|
70
|
-
strokeWidth="2"
|
|
71
|
-
strokeLinecap="round"
|
|
72
|
-
strokeLinejoin="round"
|
|
73
|
-
aria-hidden="true"
|
|
74
|
-
>
|
|
75
|
-
<path d="m12 19-7-7 7-7" />
|
|
76
|
-
<path d="M19 12H5" />
|
|
77
|
-
</svg>
|
|
78
|
-
</Link>
|
|
79
|
-
<h1 className="text-xl font-semibold text-white">Settings</h1>
|
|
80
|
-
</div>
|
|
81
|
-
</header>
|
|
82
|
-
|
|
83
|
-
{/* Settings Content */}
|
|
84
|
-
<main className="mx-auto max-w-2xl p-6">
|
|
85
|
-
{/* Tags Section */}
|
|
86
|
-
<section className="mb-8">
|
|
87
|
-
<h2 className="mb-4 text-lg font-medium text-zinc-100">Tags</h2>
|
|
88
|
-
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4">
|
|
89
|
-
<p className="text-sm text-zinc-400">
|
|
90
|
-
Manage your project tags here. Tags help organize and categorize your projects.
|
|
91
|
-
</p>
|
|
92
|
-
|
|
93
|
-
{/* Tags List */}
|
|
94
|
-
<div className="mt-4 space-y-2">
|
|
95
|
-
{isLoading ? (
|
|
96
|
-
<p className="text-sm text-zinc-400">Loading tags…</p>
|
|
97
|
-
) : tags.length === 0 && !isAddingTag ? (
|
|
98
|
-
<p className="text-sm text-zinc-400">No tags yet. Create one to get started.</p>
|
|
99
|
-
) : (
|
|
100
|
-
tags.map((tag) => (
|
|
101
|
-
<div
|
|
102
|
-
key={tag.id}
|
|
103
|
-
className="flex items-center justify-between rounded-md border border-zinc-800 bg-zinc-800/50 px-3 py-2"
|
|
104
|
-
>
|
|
105
|
-
<div className="flex items-center gap-2">
|
|
106
|
-
<span
|
|
107
|
-
className="size-4 rounded-full"
|
|
108
|
-
style={{ backgroundColor: tag.color }}
|
|
109
|
-
aria-hidden="true"
|
|
110
|
-
/>
|
|
111
|
-
<span className="text-sm font-medium text-zinc-200">{tag.name}</span>
|
|
112
|
-
</div>
|
|
113
|
-
<button
|
|
114
|
-
onClick={() => handleDeleteTag(tag.id)}
|
|
115
|
-
className="rounded p-1 text-zinc-500 hover:bg-zinc-700 hover:text-zinc-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0a0a0a]"
|
|
116
|
-
title="Delete tag"
|
|
117
|
-
aria-label={`Delete tag ${tag.name}`}
|
|
118
|
-
>
|
|
119
|
-
<svg
|
|
120
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
121
|
-
width="16"
|
|
122
|
-
height="16"
|
|
123
|
-
viewBox="0 0 24 24"
|
|
124
|
-
fill="none"
|
|
125
|
-
stroke="currentColor"
|
|
126
|
-
strokeWidth="2"
|
|
127
|
-
strokeLinecap="round"
|
|
128
|
-
strokeLinejoin="round"
|
|
129
|
-
>
|
|
130
|
-
<path d="M3 6h18" />
|
|
131
|
-
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
132
|
-
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
133
|
-
<line x1="10" x2="10" y1="11" y2="17" />
|
|
134
|
-
<line x1="14" x2="14" y1="11" y2="17" />
|
|
135
|
-
</svg>
|
|
136
|
-
</button>
|
|
137
|
-
</div>
|
|
138
|
-
))
|
|
139
|
-
)}
|
|
140
|
-
|
|
141
|
-
{/* Add Tag Form */}
|
|
142
|
-
{isAddingTag && (
|
|
143
|
-
<div className="mt-3 space-y-3 rounded-md border border-zinc-800 bg-zinc-900/70 p-3">
|
|
144
|
-
<div className="flex items-center gap-2">
|
|
145
|
-
<ColorPicker value={newTagColor} onChange={setNewTagColor} />
|
|
146
|
-
<Input
|
|
147
|
-
value={newTagName}
|
|
148
|
-
onChange={(e) => setNewTagName(e.target.value)}
|
|
149
|
-
placeholder="Tag name…"
|
|
150
|
-
aria-label="Tag name"
|
|
151
|
-
className="flex-1 border-zinc-700 bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
|
152
|
-
autoFocus
|
|
153
|
-
onKeyDown={(e) => {
|
|
154
|
-
if (e.key === "Enter") {
|
|
155
|
-
handleCreateTag();
|
|
156
|
-
} else if (e.key === "Escape") {
|
|
157
|
-
setIsAddingTag(false);
|
|
158
|
-
setNewTagName("");
|
|
159
|
-
setNewTagColor("#3b82f6");
|
|
160
|
-
}
|
|
161
|
-
}}
|
|
162
|
-
/>
|
|
163
|
-
</div>
|
|
164
|
-
<div className="flex justify-end gap-2">
|
|
165
|
-
<Button
|
|
166
|
-
variant="outline"
|
|
167
|
-
size="sm"
|
|
168
|
-
className="border-zinc-700 bg-transparent text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
|
|
169
|
-
onClick={() => {
|
|
170
|
-
setIsAddingTag(false);
|
|
171
|
-
setNewTagName("");
|
|
172
|
-
setNewTagColor("#3b82f6");
|
|
173
|
-
}}
|
|
174
|
-
>
|
|
175
|
-
Cancel
|
|
176
|
-
</Button>
|
|
177
|
-
<Button
|
|
178
|
-
size="sm"
|
|
179
|
-
className="bg-zinc-100 text-zinc-900 hover:bg-white"
|
|
180
|
-
onClick={handleCreateTag}
|
|
181
|
-
disabled={!newTagName.trim()}
|
|
182
|
-
>
|
|
183
|
-
Create Tag
|
|
184
|
-
</Button>
|
|
185
|
-
</div>
|
|
186
|
-
</div>
|
|
187
|
-
)}
|
|
188
|
-
</div>
|
|
189
|
-
|
|
190
|
-
{/* Add Tag Button */}
|
|
191
|
-
{!isAddingTag && (
|
|
192
|
-
<div className="mt-4">
|
|
193
|
-
<button
|
|
194
|
-
onClick={() => setIsAddingTag(true)}
|
|
195
|
-
className="rounded-md bg-zinc-100 px-3 py-1.5 text-sm text-zinc-900 hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0a0a0a]"
|
|
196
|
-
>
|
|
197
|
-
Add Tag
|
|
198
|
-
</button>
|
|
199
|
-
</div>
|
|
200
|
-
)}
|
|
201
|
-
</div>
|
|
202
|
-
</section>
|
|
203
|
-
|
|
204
|
-
{/* Data Section */}
|
|
205
|
-
<section className="mb-8">
|
|
206
|
-
<h2 className="mb-4 text-lg font-medium text-zinc-100">Data</h2>
|
|
207
|
-
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4">
|
|
208
|
-
<div className="flex items-center justify-between">
|
|
209
|
-
<div>
|
|
210
|
-
<p className="font-medium text-red-400">Clear Local Database</p>
|
|
211
|
-
<p className="text-sm text-zinc-400">
|
|
212
|
-
Remove all projects and tags from local storage
|
|
213
|
-
</p>
|
|
214
|
-
</div>
|
|
215
|
-
<button className="rounded-md border border-red-800/50 bg-red-900/30 px-3 py-1.5 text-sm text-red-400 hover:bg-red-900/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-400 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0a0a0a]">
|
|
216
|
-
Clear Data
|
|
217
|
-
</button>
|
|
218
|
-
</div>
|
|
219
|
-
</div>
|
|
220
|
-
</section>
|
|
221
|
-
</main>
|
|
222
|
-
</div>
|
|
223
|
-
);
|
|
224
|
-
}
|