@castlekit/castle 0.0.1 → 0.1.0
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 +38 -1
- package/bin/castle.js +94 -0
- package/install.sh +722 -0
- package/next.config.ts +7 -0
- package/package.json +54 -5
- package/postcss.config.mjs +7 -0
- package/src/app/api/avatars/[id]/route.ts +75 -0
- package/src/app/api/openclaw/agents/route.ts +107 -0
- package/src/app/api/openclaw/config/route.ts +94 -0
- package/src/app/api/openclaw/events/route.ts +96 -0
- package/src/app/api/openclaw/logs/route.ts +59 -0
- package/src/app/api/openclaw/ping/route.ts +68 -0
- package/src/app/api/openclaw/restart/route.ts +65 -0
- package/src/app/api/openclaw/sessions/route.ts +62 -0
- package/src/app/globals.css +286 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +42 -0
- package/src/app/page.tsx +269 -0
- package/src/app/ui-kit/page.tsx +684 -0
- package/src/cli/onboarding.ts +576 -0
- package/src/components/dashboard/agent-status.tsx +107 -0
- package/src/components/dashboard/glass-card.tsx +28 -0
- package/src/components/dashboard/goal-widget.tsx +174 -0
- package/src/components/dashboard/greeting-widget.tsx +78 -0
- package/src/components/dashboard/index.ts +7 -0
- package/src/components/dashboard/stat-widget.tsx +61 -0
- package/src/components/dashboard/stock-widget.tsx +164 -0
- package/src/components/dashboard/weather-widget.tsx +68 -0
- package/src/components/icons/castle-icon.tsx +21 -0
- package/src/components/kanban/index.ts +3 -0
- package/src/components/kanban/kanban-board.tsx +391 -0
- package/src/components/kanban/kanban-card.tsx +137 -0
- package/src/components/kanban/kanban-column.tsx +98 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/layout/page-header.tsx +20 -0
- package/src/components/layout/sidebar.tsx +128 -0
- package/src/components/layout/theme-toggle.tsx +59 -0
- package/src/components/layout/user-menu.tsx +72 -0
- package/src/components/ui/alert.tsx +72 -0
- package/src/components/ui/avatar.tsx +87 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/button.tsx +43 -0
- package/src/components/ui/card.tsx +107 -0
- package/src/components/ui/checkbox.tsx +56 -0
- package/src/components/ui/clock.tsx +171 -0
- package/src/components/ui/dialog.tsx +105 -0
- package/src/components/ui/index.ts +34 -0
- package/src/components/ui/input.tsx +112 -0
- package/src/components/ui/option-card.tsx +151 -0
- package/src/components/ui/progress.tsx +103 -0
- package/src/components/ui/radio.tsx +109 -0
- package/src/components/ui/select.tsx +46 -0
- package/src/components/ui/slider.tsx +62 -0
- package/src/components/ui/tabs.tsx +132 -0
- package/src/components/ui/toggle-group.tsx +85 -0
- package/src/components/ui/toggle.tsx +78 -0
- package/src/components/ui/tooltip.tsx +145 -0
- package/src/components/ui/uptime.tsx +106 -0
- package/src/lib/config.ts +195 -0
- package/src/lib/gateway-connection.ts +391 -0
- package/src/lib/hooks/use-openclaw.ts +163 -0
- package/src/lib/utils.ts +6 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from "react";
|
|
4
|
+
import { Plus, MoreHorizontal } from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
DndContext,
|
|
7
|
+
DragOverlay,
|
|
8
|
+
rectIntersection,
|
|
9
|
+
KeyboardSensor,
|
|
10
|
+
PointerSensor,
|
|
11
|
+
useSensor,
|
|
12
|
+
useSensors,
|
|
13
|
+
type DragStartEvent,
|
|
14
|
+
type DragEndEvent,
|
|
15
|
+
type DragOverEvent,
|
|
16
|
+
} from "@dnd-kit/core";
|
|
17
|
+
import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable";
|
|
18
|
+
import { cn } from "@/lib/utils";
|
|
19
|
+
import { KanbanColumn } from "./kanban-column";
|
|
20
|
+
import { KanbanCard, type KanbanCardProps } from "./kanban-card";
|
|
21
|
+
|
|
22
|
+
export interface ColumnData {
|
|
23
|
+
id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
color?: string;
|
|
26
|
+
cards: KanbanCardProps[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface KanbanBoardProps {
|
|
30
|
+
columns?: ColumnData[];
|
|
31
|
+
className?: string;
|
|
32
|
+
onCardMove?: (cardId: string, fromColumn: string, toColumn: string, newIndex: number) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const defaultColumns: ColumnData[] = [
|
|
36
|
+
{
|
|
37
|
+
id: "captured",
|
|
38
|
+
title: "Captured",
|
|
39
|
+
color: "#a3a3a3",
|
|
40
|
+
cards: [
|
|
41
|
+
{
|
|
42
|
+
id: "1",
|
|
43
|
+
title: "Add voice commands to agents",
|
|
44
|
+
description: "Explore options for voice input/output with agents",
|
|
45
|
+
labels: ["feature"],
|
|
46
|
+
priority: "low",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "2",
|
|
50
|
+
title: "Mobile app concept",
|
|
51
|
+
description: "Think about mobile experience for Castle",
|
|
52
|
+
labels: ["idea"],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "3",
|
|
56
|
+
title: "Agent memory persistence",
|
|
57
|
+
labels: ["technical"],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "vibing",
|
|
63
|
+
title: "Vibing",
|
|
64
|
+
color: "#8b5cf6",
|
|
65
|
+
cards: [
|
|
66
|
+
{
|
|
67
|
+
id: "4",
|
|
68
|
+
title: "Chess coach agent",
|
|
69
|
+
description: "Agent that helps improve chess game through analysis",
|
|
70
|
+
labels: ["agent", "learning"],
|
|
71
|
+
priority: "medium",
|
|
72
|
+
assignee: { name: "Sage" },
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "5",
|
|
76
|
+
title: "Crypto portfolio tracker",
|
|
77
|
+
description: "Track Bitcoin and other crypto holdings",
|
|
78
|
+
labels: ["app"],
|
|
79
|
+
commentCount: 3,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "scoped",
|
|
85
|
+
title: "Scoped",
|
|
86
|
+
color: "#3b82f6",
|
|
87
|
+
cards: [
|
|
88
|
+
{
|
|
89
|
+
id: "6",
|
|
90
|
+
title: "The Armory - System Dashboard",
|
|
91
|
+
description: "Dashboard showing API usage, Mac Mini health, agent stats",
|
|
92
|
+
labels: ["castle", "priority"],
|
|
93
|
+
priority: "high",
|
|
94
|
+
assignee: { name: "Max" },
|
|
95
|
+
commentCount: 5,
|
|
96
|
+
attachmentCount: 2,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "in_development",
|
|
102
|
+
title: "In Development",
|
|
103
|
+
color: "#f59e0b",
|
|
104
|
+
cards: [
|
|
105
|
+
{
|
|
106
|
+
id: "7",
|
|
107
|
+
title: "Projects - Kanban Board",
|
|
108
|
+
description: "Project management kanban for Castle app",
|
|
109
|
+
labels: ["castle", "active"],
|
|
110
|
+
priority: "urgent",
|
|
111
|
+
assignee: { name: "Mason" },
|
|
112
|
+
commentCount: 8,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "review",
|
|
118
|
+
title: "Review",
|
|
119
|
+
color: "#ec4899",
|
|
120
|
+
cards: [],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "live",
|
|
124
|
+
title: "Live",
|
|
125
|
+
color: "#22c55e",
|
|
126
|
+
cards: [
|
|
127
|
+
{
|
|
128
|
+
id: "8",
|
|
129
|
+
title: "Castle App Bootstrap",
|
|
130
|
+
description: "Initial Next.js setup with Tailwind",
|
|
131
|
+
labels: ["castle"],
|
|
132
|
+
assignee: { name: "Mason" },
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "9",
|
|
136
|
+
title: "Agent SOUL Architecture",
|
|
137
|
+
description: "Modular identity system for agents",
|
|
138
|
+
labels: ["documentation"],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
function KanbanBoard({ columns: initialColumns = defaultColumns, className, onCardMove }: KanbanBoardProps) {
|
|
145
|
+
const [columns, setColumns] = useState<ColumnData[]>(initialColumns);
|
|
146
|
+
const [activeCard, setActiveCard] = useState<KanbanCardProps | null>(null);
|
|
147
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
148
|
+
const [overColumnId, setOverColumnId] = useState<string | null>(null);
|
|
149
|
+
|
|
150
|
+
const columnsRef = useRef(columns);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
columnsRef.current = columns;
|
|
153
|
+
}, [columns]);
|
|
154
|
+
|
|
155
|
+
const isProcessing = useRef(false);
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
setIsMounted(true);
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
const sensors = useSensors(
|
|
162
|
+
useSensor(PointerSensor, {
|
|
163
|
+
activationConstraint: {
|
|
164
|
+
distance: 8,
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
useSensor(KeyboardSensor, {
|
|
168
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
function handleDragStart(event: DragStartEvent) {
|
|
173
|
+
const { active } = event;
|
|
174
|
+
for (const col of columnsRef.current) {
|
|
175
|
+
const card = col.cards.find((c) => c.id === active.id);
|
|
176
|
+
if (card) {
|
|
177
|
+
setActiveCard(card);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function handleDragOver(event: DragOverEvent) {
|
|
184
|
+
const { active, over } = event;
|
|
185
|
+
|
|
186
|
+
if (!over) {
|
|
187
|
+
setOverColumnId(null);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const activeId = active.id as string;
|
|
192
|
+
const overId = over.id as string;
|
|
193
|
+
|
|
194
|
+
const currentColumns = columnsRef.current;
|
|
195
|
+
|
|
196
|
+
const findColByCard = (cardId: string) =>
|
|
197
|
+
currentColumns.find((col) => col.cards.some((card) => card.id === cardId));
|
|
198
|
+
const findColById = (colId: string) =>
|
|
199
|
+
currentColumns.find((col) => col.id === colId);
|
|
200
|
+
|
|
201
|
+
const activeCol = findColByCard(activeId);
|
|
202
|
+
const overCol = findColByCard(overId) || findColById(overId);
|
|
203
|
+
|
|
204
|
+
if (overCol) {
|
|
205
|
+
setOverColumnId(overCol.id);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!activeCol || !overCol) return;
|
|
209
|
+
if (activeId === overId) return;
|
|
210
|
+
|
|
211
|
+
if (isProcessing.current) return;
|
|
212
|
+
|
|
213
|
+
if (activeCol.id === overCol.id) return;
|
|
214
|
+
|
|
215
|
+
const draggedCard = activeCol.cards.find((c) => c.id === activeId);
|
|
216
|
+
if (!draggedCard) return;
|
|
217
|
+
|
|
218
|
+
const overCardIndex = overCol.cards.findIndex((c) => c.id === overId);
|
|
219
|
+
const insertIndex = overCardIndex >= 0 ? overCardIndex : overCol.cards.length;
|
|
220
|
+
|
|
221
|
+
isProcessing.current = true;
|
|
222
|
+
|
|
223
|
+
setColumns((prev) => {
|
|
224
|
+
return prev.map((col) => {
|
|
225
|
+
if (col.id === activeCol.id) {
|
|
226
|
+
return { ...col, cards: col.cards.filter((c) => c.id !== activeId) };
|
|
227
|
+
}
|
|
228
|
+
if (col.id === overCol.id) {
|
|
229
|
+
const newCards = [...col.cards];
|
|
230
|
+
newCards.splice(insertIndex, 0, draggedCard);
|
|
231
|
+
return { ...col, cards: newCards };
|
|
232
|
+
}
|
|
233
|
+
return col;
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
requestAnimationFrame(() => {
|
|
238
|
+
isProcessing.current = false;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function handleDragEnd(event: DragEndEvent) {
|
|
243
|
+
const { active, over } = event;
|
|
244
|
+
|
|
245
|
+
setActiveCard(null);
|
|
246
|
+
setOverColumnId(null);
|
|
247
|
+
isProcessing.current = false;
|
|
248
|
+
|
|
249
|
+
if (!over) return;
|
|
250
|
+
|
|
251
|
+
const activeId = active.id as string;
|
|
252
|
+
const overId = over.id as string;
|
|
253
|
+
if (activeId === overId) return;
|
|
254
|
+
|
|
255
|
+
setColumns((prev) => {
|
|
256
|
+
const findColByCard = (cardId: string) =>
|
|
257
|
+
prev.find((col) => col.cards.some((card) => card.id === cardId));
|
|
258
|
+
|
|
259
|
+
const activeCol = findColByCard(activeId);
|
|
260
|
+
const overCol = findColByCard(overId);
|
|
261
|
+
|
|
262
|
+
if (!activeCol || !overCol || activeCol.id !== overCol.id) return prev;
|
|
263
|
+
|
|
264
|
+
const oldIndex = activeCol.cards.findIndex((c) => c.id === activeId);
|
|
265
|
+
const newIndex = activeCol.cards.findIndex((c) => c.id === overId);
|
|
266
|
+
|
|
267
|
+
if (oldIndex === -1 || newIndex === -1 || oldIndex === newIndex) return prev;
|
|
268
|
+
|
|
269
|
+
return prev.map((col) =>
|
|
270
|
+
col.id === activeCol.id
|
|
271
|
+
? { ...col, cards: arrayMove(col.cards, oldIndex, newIndex) }
|
|
272
|
+
: col
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (onCardMove) {
|
|
277
|
+
const currentColumns = columnsRef.current;
|
|
278
|
+
for (const col of currentColumns) {
|
|
279
|
+
const idx = col.cards.findIndex((c) => c.id === activeId);
|
|
280
|
+
if (idx >= 0) {
|
|
281
|
+
onCardMove(activeId, "", col.id, idx);
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!isMounted) {
|
|
289
|
+
return (
|
|
290
|
+
<div
|
|
291
|
+
className={cn(
|
|
292
|
+
"flex items-stretch gap-4 overflow-x-auto pb-4",
|
|
293
|
+
className
|
|
294
|
+
)}
|
|
295
|
+
>
|
|
296
|
+
{columns.map((column) => (
|
|
297
|
+
<div
|
|
298
|
+
key={column.id}
|
|
299
|
+
className="flex flex-col min-w-[280px] max-w-[320px] bg-background-secondary rounded-[var(--radius-lg)] p-3 transition-colors"
|
|
300
|
+
>
|
|
301
|
+
<div className="flex items-center justify-between mb-3 px-1">
|
|
302
|
+
<div className="flex items-center gap-2">
|
|
303
|
+
{column.color && (
|
|
304
|
+
<div
|
|
305
|
+
className="h-3 w-3 rounded-[var(--radius-full)]"
|
|
306
|
+
style={{ backgroundColor: column.color }}
|
|
307
|
+
/>
|
|
308
|
+
)}
|
|
309
|
+
<h3 className="text-sm font-medium text-foreground">{column.title}</h3>
|
|
310
|
+
<span className="text-xs text-foreground-muted bg-surface px-1.5 py-0.5 rounded-[var(--radius-sm)]">
|
|
311
|
+
{column.cards.length}
|
|
312
|
+
</span>
|
|
313
|
+
</div>
|
|
314
|
+
<div className="flex items-center gap-1">
|
|
315
|
+
<button
|
|
316
|
+
className="p-1 rounded-[var(--radius-sm)] hover:bg-surface transition-colors cursor-pointer"
|
|
317
|
+
disabled
|
|
318
|
+
>
|
|
319
|
+
<Plus className="h-4 w-4 text-foreground-muted" />
|
|
320
|
+
</button>
|
|
321
|
+
<button
|
|
322
|
+
className="p-1 rounded-[var(--radius-sm)] hover:bg-surface transition-colors cursor-pointer"
|
|
323
|
+
disabled
|
|
324
|
+
>
|
|
325
|
+
<MoreHorizontal className="h-4 w-4 text-foreground-muted" />
|
|
326
|
+
</button>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div className="flex flex-col gap-2 flex-1 overflow-y-auto rounded-[var(--radius-md)] p-2 -m-1 border border-dashed border-transparent transition-colors">
|
|
331
|
+
{column.cards.map((card) => (
|
|
332
|
+
<KanbanCard key={card.id} {...card} />
|
|
333
|
+
))}
|
|
334
|
+
{column.cards.length === 0 && (
|
|
335
|
+
<div className="flex items-center justify-center h-20 text-sm text-foreground-muted">
|
|
336
|
+
No tasks
|
|
337
|
+
</div>
|
|
338
|
+
)}
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<button
|
|
342
|
+
className="flex items-center justify-center gap-2 mt-3 p-2 text-sm text-foreground-secondary hover:text-foreground hover:bg-surface rounded-[var(--radius-md)] transition-colors cursor-pointer"
|
|
343
|
+
>
|
|
344
|
+
<Plus className="h-4 w-4" />
|
|
345
|
+
Add task
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
348
|
+
))}
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<DndContext
|
|
355
|
+
sensors={sensors}
|
|
356
|
+
collisionDetection={rectIntersection}
|
|
357
|
+
onDragStart={handleDragStart}
|
|
358
|
+
onDragOver={handleDragOver}
|
|
359
|
+
onDragEnd={handleDragEnd}
|
|
360
|
+
>
|
|
361
|
+
<div
|
|
362
|
+
className={cn(
|
|
363
|
+
"flex items-stretch gap-4 overflow-x-auto pb-4",
|
|
364
|
+
className
|
|
365
|
+
)}
|
|
366
|
+
>
|
|
367
|
+
{columns.map((column) => (
|
|
368
|
+
<KanbanColumn
|
|
369
|
+
key={column.id}
|
|
370
|
+
id={column.id}
|
|
371
|
+
title={column.title}
|
|
372
|
+
color={column.color}
|
|
373
|
+
cards={column.cards}
|
|
374
|
+
count={column.cards.length}
|
|
375
|
+
isDropTarget={overColumnId === column.id}
|
|
376
|
+
/>
|
|
377
|
+
))}
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<DragOverlay dropAnimation={null}>
|
|
381
|
+
{activeCard ? (
|
|
382
|
+
<div className="shadow-xl rotate-2">
|
|
383
|
+
<KanbanCard {...activeCard} />
|
|
384
|
+
</div>
|
|
385
|
+
) : null}
|
|
386
|
+
</DragOverlay>
|
|
387
|
+
</DndContext>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export { KanbanBoard };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSortable } from "@dnd-kit/sortable";
|
|
4
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
5
|
+
import { MoreHorizontal, MessageSquare, Paperclip } from "lucide-react";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
import { Badge } from "@/components/ui/badge";
|
|
8
|
+
|
|
9
|
+
export type TaskPriority = "low" | "medium" | "high" | "urgent";
|
|
10
|
+
export type TaskStatus =
|
|
11
|
+
| "captured"
|
|
12
|
+
| "vibing"
|
|
13
|
+
| "scoped"
|
|
14
|
+
| "ready"
|
|
15
|
+
| "in_development"
|
|
16
|
+
| "review"
|
|
17
|
+
| "live";
|
|
18
|
+
|
|
19
|
+
export interface KanbanCardProps {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
priority?: TaskPriority;
|
|
24
|
+
assignee?: {
|
|
25
|
+
name: string;
|
|
26
|
+
avatar?: string;
|
|
27
|
+
};
|
|
28
|
+
labels?: string[];
|
|
29
|
+
commentCount?: number;
|
|
30
|
+
attachmentCount?: number;
|
|
31
|
+
className?: string;
|
|
32
|
+
onClick?: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function KanbanCard({
|
|
36
|
+
title,
|
|
37
|
+
description,
|
|
38
|
+
assignee,
|
|
39
|
+
labels = [],
|
|
40
|
+
commentCount = 0,
|
|
41
|
+
attachmentCount = 0,
|
|
42
|
+
className,
|
|
43
|
+
onClick,
|
|
44
|
+
}: KanbanCardProps) {
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
className={cn(
|
|
48
|
+
"group relative rounded-[var(--radius-md)] bg-surface border border-border p-4 transition-shadow cursor-pointer",
|
|
49
|
+
"hover:border-border-hover hover:shadow-md hover:shadow-black/5 dark:hover:shadow-black/20",
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
onClick={onClick}
|
|
53
|
+
>
|
|
54
|
+
<button
|
|
55
|
+
className="absolute right-2 top-2 p-1 rounded-[var(--radius-sm)] opacity-0 group-hover:opacity-100 hover:bg-surface-hover transition-opacity"
|
|
56
|
+
onClick={(e) => {
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<MoreHorizontal className="h-4 w-4 text-foreground-muted" />
|
|
61
|
+
</button>
|
|
62
|
+
|
|
63
|
+
{labels.length > 0 && (
|
|
64
|
+
<div className="flex flex-wrap gap-1 mb-3">
|
|
65
|
+
{labels.map((label) => (
|
|
66
|
+
<Badge key={label} variant="outline" size="sm">
|
|
67
|
+
{label}
|
|
68
|
+
</Badge>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<h4 className="text-sm font-medium text-foreground pr-6 mb-1">{title}</h4>
|
|
74
|
+
|
|
75
|
+
{description && (
|
|
76
|
+
<p className="text-xs text-foreground-secondary line-clamp-2 mb-3">
|
|
77
|
+
{description}
|
|
78
|
+
</p>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
<div className="flex items-center justify-between mt-3 pt-3 border-t border-border">
|
|
82
|
+
<div className="flex items-center gap-3">
|
|
83
|
+
{commentCount > 0 && (
|
|
84
|
+
<div className="flex items-center gap-1 text-foreground-muted">
|
|
85
|
+
<MessageSquare className="h-3.5 w-3.5" />
|
|
86
|
+
<span className="text-xs">{commentCount}</span>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{attachmentCount > 0 && (
|
|
90
|
+
<div className="flex items-center gap-1 text-foreground-muted">
|
|
91
|
+
<Paperclip className="h-3.5 w-3.5" />
|
|
92
|
+
<span className="text-xs">{attachmentCount}</span>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{assignee && (
|
|
98
|
+
<div className="h-6 w-6 rounded-[var(--radius-full)] bg-surface-hover flex items-center justify-center text-xs font-medium text-foreground-secondary border border-border">
|
|
99
|
+
{assignee.name[0]}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function SortableCard(props: KanbanCardProps) {
|
|
108
|
+
const {
|
|
109
|
+
attributes,
|
|
110
|
+
listeners,
|
|
111
|
+
setNodeRef,
|
|
112
|
+
transform,
|
|
113
|
+
isDragging,
|
|
114
|
+
} = useSortable({ id: props.id });
|
|
115
|
+
|
|
116
|
+
const style = {
|
|
117
|
+
transform: CSS.Transform.toString(transform),
|
|
118
|
+
opacity: isDragging ? 0.5 : 1,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
ref={setNodeRef}
|
|
124
|
+
style={style}
|
|
125
|
+
{...attributes}
|
|
126
|
+
{...listeners}
|
|
127
|
+
className={cn(
|
|
128
|
+
"touch-none",
|
|
129
|
+
isDragging && "z-50"
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
<KanbanCard {...props} />
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { KanbanCard, SortableCard };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useDroppable } from "@dnd-kit/core";
|
|
4
|
+
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
5
|
+
import { Plus, MoreHorizontal } from "lucide-react";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
import { SortableCard, type KanbanCardProps } from "./kanban-card";
|
|
8
|
+
|
|
9
|
+
export interface KanbanColumnProps {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
count?: number;
|
|
13
|
+
color?: string;
|
|
14
|
+
cards?: KanbanCardProps[];
|
|
15
|
+
className?: string;
|
|
16
|
+
onAddCard?: () => void;
|
|
17
|
+
isDropTarget?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function KanbanColumn({
|
|
21
|
+
id,
|
|
22
|
+
title,
|
|
23
|
+
count = 0,
|
|
24
|
+
color,
|
|
25
|
+
cards = [],
|
|
26
|
+
className,
|
|
27
|
+
onAddCard,
|
|
28
|
+
isDropTarget = false,
|
|
29
|
+
}: KanbanColumnProps) {
|
|
30
|
+
const { setNodeRef } = useDroppable({
|
|
31
|
+
id,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
className={cn(
|
|
37
|
+
"flex flex-col min-w-[280px] max-w-[320px] bg-background-secondary rounded-[var(--radius-lg)] p-3 transition-colors",
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
<div className="flex items-center justify-between mb-3 px-1">
|
|
42
|
+
<div className="flex items-center gap-2">
|
|
43
|
+
{color && (
|
|
44
|
+
<div
|
|
45
|
+
className="h-3 w-3 rounded-[var(--radius-full)]"
|
|
46
|
+
style={{ backgroundColor: color }}
|
|
47
|
+
/>
|
|
48
|
+
)}
|
|
49
|
+
<h3 className="text-sm font-medium text-foreground">{title}</h3>
|
|
50
|
+
<span className="text-xs text-foreground-muted bg-surface px-1.5 py-0.5 rounded-[var(--radius-sm)]">
|
|
51
|
+
{count}
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="flex items-center gap-1">
|
|
55
|
+
<button
|
|
56
|
+
className="p-1 rounded-[var(--radius-sm)] hover:bg-surface transition-colors cursor-pointer"
|
|
57
|
+
onClick={onAddCard}
|
|
58
|
+
>
|
|
59
|
+
<Plus className="h-4 w-4 text-foreground-muted" />
|
|
60
|
+
</button>
|
|
61
|
+
<button className="p-1 rounded-[var(--radius-sm)] hover:bg-surface transition-colors cursor-pointer">
|
|
62
|
+
<MoreHorizontal className="h-4 w-4 text-foreground-muted" />
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div
|
|
68
|
+
ref={setNodeRef}
|
|
69
|
+
className={cn(
|
|
70
|
+
"flex flex-col gap-2 flex-1 overflow-y-auto rounded-[var(--radius-md)] p-2 -m-1 border border-dashed border-transparent transition-colors",
|
|
71
|
+
isDropTarget && "border-accent/50 bg-accent/10"
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
<SortableContext items={cards.map((c) => c.id)} strategy={verticalListSortingStrategy}>
|
|
75
|
+
{cards.map((card) => (
|
|
76
|
+
<SortableCard key={card.id} {...card} />
|
|
77
|
+
))}
|
|
78
|
+
</SortableContext>
|
|
79
|
+
|
|
80
|
+
{cards.length === 0 && (
|
|
81
|
+
<div className="flex items-center justify-center h-20 text-sm text-foreground-muted">
|
|
82
|
+
No tasks
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<button
|
|
88
|
+
className="flex items-center justify-center gap-2 mt-3 p-2 text-sm text-foreground-secondary hover:text-foreground hover:bg-surface rounded-[var(--radius-md)] transition-colors cursor-pointer"
|
|
89
|
+
onClick={onAddCard}
|
|
90
|
+
>
|
|
91
|
+
<Plus className="h-4 w-4" />
|
|
92
|
+
Add task
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { KanbanColumn };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
|
|
3
|
+
export interface PageHeaderProps {
|
|
4
|
+
title: string;
|
|
5
|
+
subtitle?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function PageHeader({ title, subtitle, className }: PageHeaderProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className={cn("space-y-2", className)}>
|
|
12
|
+
<h1 className="text-2xl font-semibold text-foreground">{title}</h1>
|
|
13
|
+
{subtitle ? (
|
|
14
|
+
<p className="text-sm text-foreground-secondary">{subtitle}</p>
|
|
15
|
+
) : null}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { PageHeader };
|