@leanspec/ui 0.2.5-dev.20251119032109 → 0.2.5-dev.20251119062438
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/.next/standalone/node_modules/.pnpm/tiktoken@1.0.22/node_modules/tiktoken/tiktoken_bg.wasm +0 -0
- package/.next/standalone/packages/ui/.next/BUILD_ID +1 -1
- package/.next/standalone/packages/ui/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/packages/ui/.next/build-manifest.json +2 -2
- package/.next/standalone/packages/ui/.next/prerender-manifest.json +3 -3
- package/.next/standalone/packages/ui/.next/routes-manifest.json +8 -0
- package/.next/standalone/packages/ui/.next/server/app/_global-error/page.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error.html +2 -2
- package/.next/standalone/packages/ui/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_not-found/page.js +2 -2
- package/.next/standalone/packages/ui/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/packages/ui/.next/server/app/_not-found.rsc +16 -16
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_full.segment.rsc +16 -16
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_tree.segment.rsc +5 -5
- package/.next/standalone/packages/ui/.next/server/app/api/projects/[id]/specs/route.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/api/projects/[id]/specs/route.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/api/projects/route.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/api/revalidate/route.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/api/revalidate/route.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/dependency-graph/route.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/dependency-graph/route.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/route.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/route.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/status/route/app-paths-manifest.json +3 -0
- package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/status/route/build-manifest.json +11 -0
- package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/status/route/server-reference-manifest.json +4 -0
- package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/status/route.js +7 -0
- package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/status/route.js.map +5 -0
- package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/status/route.js.nft.json +1 -0
- package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/status/route_client-reference-manifest.js +2 -0
- package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/subspecs/[file]/route.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/subspecs/[file]/route.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/api/stats/route.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/api/stats/route.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/page.js +2 -2
- package/.next/standalone/packages/ui/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page.js +2 -2
- package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/specs/page.js +2 -2
- package/.next/standalone/packages/ui/.next/server/app/specs/page.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/specs/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/stats/page.js +2 -2
- package/.next/standalone/packages/ui/.next/server/app/stats/page.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/stats/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/packages/ui/.next/server/chunks/730ea_ui__next-internal_server_app_api_specs_[id]_status_route_actions_5d700407.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/{[root-of-the-server]__d1d92c7c._.js → [root-of-the-server]__175bef84._.js} +2 -2
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__3971eae5._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__6bca1621._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__87a3475a._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__9f0f4c0b._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__c1c9f5f5._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/{[root-of-the-server]__e09ce4bf._.js → [root-of-the-server]__c6689757._.js} +2 -2
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__cd1fb0a2._.js +4 -0
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__e2071b2e._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/e0876_tiktoken_3efea2dc._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__1d0c2012._.js +1 -1
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/{[root-of-the-server]__c65aedd0._.js → [root-of-the-server]__299c81cc._.js} +2 -2
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/{[root-of-the-server]__b3633d6e._.js → [root-of-the-server]__41f5b5c0._.js} +2 -2
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/{[root-of-the-server]__a9d7fd42._.js → [root-of-the-server]__5ca2e973._.js} +2 -2
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__7f55abaa._.js +1 -1
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__9d8d3fd6._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__e79a982a._.js +7 -0
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/{[root-of-the-server]__6e89d129._.js → [root-of-the-server]__ff03fc1e._.js} +2 -2
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/_000dd317._.js +1 -1
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/_6cd9a5e0._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/_b483c9fe._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/_b5040d31._.js +5 -0
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/_e1889307._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_app_specs_specs-client_tsx_0bb8f8f8._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_components_specs-nav-sidebar_tsx_8237ed13._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/pages/404.html +2 -2
- package/.next/standalone/packages/ui/.next/server/pages/500.html +2 -2
- package/.next/standalone/packages/ui/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/packages/ui/.next/static/chunks/061e3819fd59154d.js +1 -0
- package/.next/standalone/packages/ui/.next/static/chunks/148ab58e68b383da.js +1 -0
- package/.next/standalone/packages/ui/.next/static/chunks/5f97d665a4d9dc62.js +3 -0
- package/.next/standalone/packages/ui/.next/static/chunks/9b54fc05b02c39e6.css +1 -0
- package/.next/standalone/packages/ui/.next/static/chunks/9b7b7024c39c5f99.js +1 -0
- package/.next/standalone/packages/ui/.next/static/chunks/{f2151fb88f8fc70a.js → ae50cdf3322e1d02.js} +1 -1
- package/.next/standalone/packages/ui/.next/static/chunks/d79bf953f0dfb650.js +1 -0
- package/.next/standalone/packages/ui/.next/static/chunks/e50fb8d0b728cd35.js +5 -0
- package/.next/standalone/packages/ui/.next/static/chunks/e6e238dbf1d3e740.js +1 -0
- package/.next/standalone/packages/ui/package.json +1 -1
- package/.next/standalone/packages/ui/src/app/api/specs/[id]/status/route.ts +122 -0
- package/.next/standalone/packages/ui/src/app/globals.css +10 -0
- package/.next/standalone/packages/ui/src/app/specs/page.tsx +2 -2
- package/.next/standalone/packages/ui/src/app/specs/specs-client.tsx +351 -104
- package/.next/standalone/packages/ui/src/lib/db/service-queries.ts +23 -0
- package/.next/standalone/packages/ui/src/lib/specs/sources/filesystem-source.ts +46 -6
- package/.next/standalone/packages/ui/tsconfig.json +2 -1
- package/.next/standalone/packages/ui/tsconfig.tsbuildinfo +1 -1
- package/.next/standalone/packages/ui/vitest.config.ts +1 -0
- package/.next/static/chunks/061e3819fd59154d.js +1 -0
- package/.next/static/chunks/148ab58e68b383da.js +1 -0
- package/.next/static/chunks/5f97d665a4d9dc62.js +3 -0
- package/.next/static/chunks/9b54fc05b02c39e6.css +1 -0
- package/.next/static/chunks/9b7b7024c39c5f99.js +1 -0
- package/.next/static/chunks/{f2151fb88f8fc70a.js → ae50cdf3322e1d02.js} +1 -1
- package/.next/static/chunks/d79bf953f0dfb650.js +1 -0
- package/.next/static/chunks/e50fb8d0b728cd35.js +5 -0
- package/.next/static/chunks/e6e238dbf1d3e740.js +1 -0
- package/package.json +1 -1
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__15e8a946._.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__47b8cab4._.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__9fda1bdb._.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__a2c55ffb._.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__d4193584._.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__68015eee._.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__8ae28367._.js +0 -7
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/_a527620e._.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/_d7e4bd50._.js +0 -5
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/_f8b2190a._.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_components_ui_input_tsx_190a7701._.js +0 -3
- package/.next/standalone/packages/ui/.next/static/chunks/16b2f4261d3a374a.css +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/17ac52d909def83e.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/3385e291c49e782d.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/5057c3e1e7e13ca3.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/88f695d32910a4ac.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/dde5d8bddd7fbfcf.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/e237e00fd3a84178.js +0 -3
- package/.next/standalone/packages/ui/.next/static/chunks/ee4245fe6ba3d6d3.js +0 -5
- package/.next/static/chunks/16b2f4261d3a374a.css +0 -1
- package/.next/static/chunks/17ac52d909def83e.js +0 -1
- package/.next/static/chunks/3385e291c49e782d.js +0 -1
- package/.next/static/chunks/5057c3e1e7e13ca3.js +0 -1
- package/.next/static/chunks/88f695d32910a4ac.js +0 -1
- package/.next/static/chunks/dde5d8bddd7fbfcf.js +0 -1
- package/.next/static/chunks/e237e00fd3a84178.js +0 -3
- package/.next/static/chunks/ee4245fe6ba3d6d3.js +0 -5
- /package/.next/standalone/packages/ui/.next/static/{uj2-H8ytNCFa611gjl5uE → 18365Lfcsz3xqXCT_JLBo}/_buildManifest.js +0 -0
- /package/.next/standalone/packages/ui/.next/static/{uj2-H8ytNCFa611gjl5uE → 18365Lfcsz3xqXCT_JLBo}/_clientMiddlewareManifest.json +0 -0
- /package/.next/standalone/packages/ui/.next/static/{uj2-H8ytNCFa611gjl5uE → 18365Lfcsz3xqXCT_JLBo}/_ssgManifest.js +0 -0
- /package/.next/static/{uj2-H8ytNCFa611gjl5uE → 18365Lfcsz3xqXCT_JLBo}/_buildManifest.js +0 -0
- /package/.next/static/{uj2-H8ytNCFa611gjl5uE → 18365Lfcsz3xqXCT_JLBo}/_clientMiddlewareManifest.json +0 -0
- /package/.next/static/{uj2-H8ytNCFa611gjl5uE → 18365Lfcsz3xqXCT_JLBo}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useMemo, useEffect, useRef } from 'react';
|
|
3
|
+
import { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import type { DragEvent } from 'react';
|
|
4
5
|
import Link from 'next/link';
|
|
5
6
|
import { useSearchParams, useRouter } from 'next/navigation';
|
|
6
7
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
@@ -19,22 +20,75 @@ import {
|
|
|
19
20
|
CheckCircle2,
|
|
20
21
|
PlayCircle,
|
|
21
22
|
Clock,
|
|
23
|
+
Archive,
|
|
22
24
|
LayoutGrid,
|
|
23
|
-
List as ListIcon
|
|
25
|
+
List as ListIcon,
|
|
26
|
+
FileText,
|
|
27
|
+
GitBranch
|
|
24
28
|
} from 'lucide-react';
|
|
25
29
|
import { StatusBadge } from '@/components/status-badge';
|
|
26
30
|
import { PriorityBadge } from '@/components/priority-badge';
|
|
27
31
|
import { cn } from '@/lib/utils';
|
|
32
|
+
import { formatRelativeTime } from '@/lib/date-utils';
|
|
33
|
+
import { toast } from '@/components/ui/toast';
|
|
34
|
+
|
|
35
|
+
type SpecStatus = 'planned' | 'in-progress' | 'complete' | 'archived';
|
|
36
|
+
|
|
37
|
+
const STATUS_CONFIG: Record<SpecStatus, {
|
|
38
|
+
icon: typeof Clock;
|
|
39
|
+
title: string;
|
|
40
|
+
colorClass: string;
|
|
41
|
+
bgClass: string;
|
|
42
|
+
borderClass: string;
|
|
43
|
+
}> = {
|
|
44
|
+
'planned': {
|
|
45
|
+
icon: Clock,
|
|
46
|
+
title: 'Planned',
|
|
47
|
+
colorClass: 'text-blue-600 dark:text-blue-400',
|
|
48
|
+
bgClass: 'bg-blue-50 dark:bg-blue-900/20',
|
|
49
|
+
borderClass: 'border-blue-200 dark:border-blue-800'
|
|
50
|
+
},
|
|
51
|
+
'in-progress': {
|
|
52
|
+
icon: PlayCircle,
|
|
53
|
+
title: 'In Progress',
|
|
54
|
+
colorClass: 'text-orange-600 dark:text-orange-400',
|
|
55
|
+
bgClass: 'bg-orange-50 dark:bg-orange-900/20',
|
|
56
|
+
borderClass: 'border-orange-200 dark:border-orange-800'
|
|
57
|
+
},
|
|
58
|
+
'complete': {
|
|
59
|
+
icon: CheckCircle2,
|
|
60
|
+
title: 'Complete',
|
|
61
|
+
colorClass: 'text-green-600 dark:text-green-400',
|
|
62
|
+
bgClass: 'bg-green-50 dark:bg-green-900/20',
|
|
63
|
+
borderClass: 'border-green-200 dark:border-green-800'
|
|
64
|
+
},
|
|
65
|
+
'archived': {
|
|
66
|
+
icon: Archive,
|
|
67
|
+
title: 'Archived',
|
|
68
|
+
colorClass: 'text-gray-600 dark:text-gray-400',
|
|
69
|
+
bgClass: 'bg-gray-50 dark:bg-gray-900/20',
|
|
70
|
+
borderClass: 'border-gray-200 dark:border-gray-800'
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const BOARD_STATUSES: SpecStatus[] = ['planned', 'in-progress', 'complete', 'archived'];
|
|
75
|
+
|
|
76
|
+
interface SpecRelationships {
|
|
77
|
+
dependsOn: string[];
|
|
78
|
+
related: string[];
|
|
79
|
+
}
|
|
28
80
|
|
|
29
81
|
interface Spec {
|
|
30
82
|
id: string;
|
|
31
83
|
specNumber: number | null;
|
|
32
84
|
specName: string;
|
|
33
85
|
title: string | null;
|
|
34
|
-
status:
|
|
86
|
+
status: SpecStatus | null;
|
|
35
87
|
priority: string | null;
|
|
36
88
|
tags: string[] | null;
|
|
37
89
|
updatedAt: Date | null;
|
|
90
|
+
subSpecsCount?: number;
|
|
91
|
+
relationships?: SpecRelationships;
|
|
38
92
|
}
|
|
39
93
|
|
|
40
94
|
interface Stats {
|
|
@@ -55,10 +109,13 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
|
|
|
55
109
|
const searchParams = useSearchParams();
|
|
56
110
|
const router = useRouter();
|
|
57
111
|
|
|
112
|
+
const [specs, setSpecs] = useState<Spec[]>(initialSpecs);
|
|
113
|
+
const [pendingSpecIds, setPendingSpecIds] = useState<Record<string, boolean>>({});
|
|
58
114
|
const [searchQuery, setSearchQuery] = useState('');
|
|
59
|
-
const [statusFilter, setStatusFilter] = useState<
|
|
115
|
+
const [statusFilter, setStatusFilter] = useState<'all' | SpecStatus>('all');
|
|
60
116
|
const [priorityFilter, setPriorityFilter] = useState<string>('all');
|
|
61
117
|
const [sortBy, setSortBy] = useState<SortBy>('id-desc');
|
|
118
|
+
const [showArchivedBoard, setShowArchivedBoard] = useState(false); // Start collapsed
|
|
62
119
|
const [viewMode, setViewMode] = useState<ViewMode>(() => {
|
|
63
120
|
// Initialize from URL or localStorage
|
|
64
121
|
const urlView = searchParams.get('view');
|
|
@@ -73,6 +130,55 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
|
|
|
73
130
|
|
|
74
131
|
const isFirstRender = useRef(true);
|
|
75
132
|
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
setSpecs(initialSpecs);
|
|
135
|
+
}, [initialSpecs]);
|
|
136
|
+
|
|
137
|
+
const handleStatusChange = useCallback(async (spec: Spec, nextStatus: SpecStatus) => {
|
|
138
|
+
if (spec.status === nextStatus) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const previousStatus = spec.status;
|
|
143
|
+
setPendingSpecIds((prev) => ({ ...prev, [spec.id]: true }));
|
|
144
|
+
setSpecs((prev) => prev.map(item => item.id === spec.id ? { ...item, status: nextStatus } : item));
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch(`/api/specs/${encodeURIComponent(spec.specName)}/status`, {
|
|
148
|
+
method: 'PATCH',
|
|
149
|
+
headers: {
|
|
150
|
+
'Content-Type': 'application/json',
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({ status: nextStatus }),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
const message = await response.text();
|
|
157
|
+
throw new Error(message || 'Failed to update spec status');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const displayName = spec.specNumber ? `#${spec.specNumber}` : spec.specName;
|
|
161
|
+
toast.success(`Moved ${displayName} to ${STATUS_CONFIG[nextStatus].title}`);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('Failed to update spec status', error);
|
|
164
|
+
setSpecs((prev) => prev.map(item => item.id === spec.id ? { ...item, status: previousStatus } : item));
|
|
165
|
+
toast.error('Unable to update status. Please try again.');
|
|
166
|
+
} finally {
|
|
167
|
+
setPendingSpecIds((prev) => {
|
|
168
|
+
const next = { ...prev };
|
|
169
|
+
delete next[spec.id];
|
|
170
|
+
return next;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}, []);
|
|
174
|
+
|
|
175
|
+
// Auto-show archived column when filtering by archived status in board view
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (statusFilter === 'archived' && viewMode === 'board') {
|
|
178
|
+
setShowArchivedBoard(true);
|
|
179
|
+
}
|
|
180
|
+
}, [statusFilter, viewMode]);
|
|
181
|
+
|
|
76
182
|
// Update URL when view mode changes (skip on initial mount)
|
|
77
183
|
useEffect(() => {
|
|
78
184
|
if (isFirstRender.current) {
|
|
@@ -97,35 +203,39 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
|
|
|
97
203
|
}, [viewMode, router]);
|
|
98
204
|
|
|
99
205
|
const filteredAndSortedSpecs = useMemo(() => {
|
|
100
|
-
const
|
|
206
|
+
const filtered = specs.filter(spec => {
|
|
101
207
|
const matchesSearch = !searchQuery ||
|
|
102
208
|
spec.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
103
209
|
spec.specName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
104
210
|
spec.tags?.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
105
211
|
|
|
106
|
-
const matchesStatus = statusFilter === 'all'
|
|
212
|
+
const matchesStatus = statusFilter === 'all'
|
|
213
|
+
? (viewMode === 'list' ? spec.status !== 'archived' : true)
|
|
214
|
+
: spec.status === statusFilter;
|
|
107
215
|
const matchesPriority = priorityFilter === 'all' || spec.priority === priorityFilter;
|
|
108
216
|
|
|
109
217
|
return matchesSearch && matchesStatus && matchesPriority;
|
|
110
218
|
});
|
|
111
219
|
|
|
220
|
+
const sorted = [...filtered];
|
|
221
|
+
|
|
112
222
|
// Sort
|
|
113
223
|
switch (sortBy) {
|
|
114
224
|
case 'id-desc':
|
|
115
|
-
|
|
225
|
+
sorted.sort((a, b) => (b.specNumber || 0) - (a.specNumber || 0));
|
|
116
226
|
break;
|
|
117
227
|
case 'id-asc':
|
|
118
|
-
|
|
228
|
+
sorted.sort((a, b) => (a.specNumber || 0) - (b.specNumber || 0));
|
|
119
229
|
break;
|
|
120
230
|
case 'updated-desc':
|
|
121
|
-
|
|
231
|
+
sorted.sort((a, b) => {
|
|
122
232
|
if (!a.updatedAt) return 1;
|
|
123
233
|
if (!b.updatedAt) return -1;
|
|
124
234
|
return b.updatedAt.getTime() - a.updatedAt.getTime();
|
|
125
235
|
});
|
|
126
236
|
break;
|
|
127
237
|
case 'title-asc':
|
|
128
|
-
|
|
238
|
+
sorted.sort((a, b) => {
|
|
129
239
|
const titleA = (a.title || a.specName).toLowerCase();
|
|
130
240
|
const titleB = (b.title || b.specName).toLowerCase();
|
|
131
241
|
return titleA.localeCompare(titleB);
|
|
@@ -133,8 +243,8 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
|
|
|
133
243
|
break;
|
|
134
244
|
}
|
|
135
245
|
|
|
136
|
-
return
|
|
137
|
-
}, [
|
|
246
|
+
return sorted;
|
|
247
|
+
}, [specs, searchQuery, statusFilter, priorityFilter, sortBy, viewMode]);
|
|
138
248
|
|
|
139
249
|
return (
|
|
140
250
|
<div className="min-h-screen bg-background p-8">
|
|
@@ -161,7 +271,7 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
|
|
|
161
271
|
</div>
|
|
162
272
|
|
|
163
273
|
{/* Status Filter */}
|
|
164
|
-
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
274
|
+
<Select value={statusFilter} onValueChange={(value) => setStatusFilter(value as SpecStatus | 'all')}>
|
|
165
275
|
<SelectTrigger className="w-full sm:w-[180px]">
|
|
166
276
|
<SelectValue placeholder="Status" />
|
|
167
277
|
</SelectTrigger>
|
|
@@ -189,54 +299,62 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
|
|
|
189
299
|
</Select>
|
|
190
300
|
</div>
|
|
191
301
|
|
|
192
|
-
<div className="flex flex-col sm:flex-row justify-between gap-4">
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
302
|
+
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
|
303
|
+
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 flex-1">
|
|
304
|
+
{/* Sort Controls */}
|
|
305
|
+
<Select value={sortBy} onValueChange={(v) => setSortBy(v as SortBy)}>
|
|
306
|
+
<SelectTrigger className="w-full sm:w-[220px]">
|
|
307
|
+
<SelectValue placeholder="Sort by" />
|
|
308
|
+
</SelectTrigger>
|
|
309
|
+
<SelectContent>
|
|
310
|
+
<SelectItem value="id-desc">Newest First (ID ↓)</SelectItem>
|
|
311
|
+
<SelectItem value="id-asc">Oldest First (ID ↑)</SelectItem>
|
|
312
|
+
<SelectItem value="updated-desc">Recently Updated</SelectItem>
|
|
313
|
+
<SelectItem value="title-asc">Title (A-Z)</SelectItem>
|
|
314
|
+
</SelectContent>
|
|
315
|
+
</Select>
|
|
205
316
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
317
|
+
{/* View Mode Switcher */}
|
|
318
|
+
<div className="flex gap-2">
|
|
319
|
+
<Button
|
|
320
|
+
variant={viewMode === 'list' ? 'default' : 'outline'}
|
|
321
|
+
size="sm"
|
|
322
|
+
onClick={() => setViewMode('list')}
|
|
323
|
+
className="flex items-center gap-2"
|
|
324
|
+
>
|
|
325
|
+
<ListIcon className="h-4 w-4" />
|
|
326
|
+
List
|
|
327
|
+
</Button>
|
|
328
|
+
<Button
|
|
329
|
+
variant={viewMode === 'board' ? 'default' : 'outline'}
|
|
330
|
+
size="sm"
|
|
331
|
+
onClick={() => setViewMode('board')}
|
|
332
|
+
className="flex items-center gap-2"
|
|
333
|
+
>
|
|
334
|
+
<LayoutGrid className="h-4 w-4" />
|
|
335
|
+
Board
|
|
336
|
+
</Button>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
{/* Results count */}
|
|
340
|
+
<div className="text-sm text-muted-foreground">
|
|
341
|
+
Showing {filteredAndSortedSpecs.length} of {specs.length} specs
|
|
342
|
+
</div>
|
|
226
343
|
</div>
|
|
227
344
|
</div>
|
|
228
345
|
</div>
|
|
229
346
|
|
|
230
|
-
{/* Results count */}
|
|
231
|
-
<div className="text-sm text-muted-foreground mb-4">
|
|
232
|
-
Showing {filteredAndSortedSpecs.length} of {initialSpecs.length} specs
|
|
233
|
-
</div>
|
|
234
|
-
|
|
235
347
|
{/* Content based on view mode */}
|
|
236
348
|
{viewMode === 'list' ? (
|
|
237
349
|
<ListView specs={filteredAndSortedSpecs} />
|
|
238
350
|
) : (
|
|
239
|
-
<BoardView
|
|
351
|
+
<BoardView
|
|
352
|
+
specs={filteredAndSortedSpecs}
|
|
353
|
+
onStatusChange={handleStatusChange}
|
|
354
|
+
pendingSpecIds={pendingSpecIds}
|
|
355
|
+
showArchived={showArchivedBoard}
|
|
356
|
+
onToggleArchived={() => setShowArchivedBoard(!showArchivedBoard)}
|
|
357
|
+
/>
|
|
240
358
|
)}
|
|
241
359
|
</div>
|
|
242
360
|
</div>
|
|
@@ -254,6 +372,8 @@ function ListView({ specs }: { specs: Spec[] }) {
|
|
|
254
372
|
'low': 'border-l-gray-400'
|
|
255
373
|
};
|
|
256
374
|
const borderColor = priorityColors[spec.priority as keyof typeof priorityColors] || 'border-l-gray-300';
|
|
375
|
+
const hasDependencies = spec.relationships && (spec.relationships.dependsOn.length > 0 || spec.relationships.related.length > 0);
|
|
376
|
+
const hasSubSpecs = !!(spec.subSpecsCount && spec.subSpecsCount > 0);
|
|
257
377
|
|
|
258
378
|
return (
|
|
259
379
|
<Card
|
|
@@ -274,6 +394,9 @@ function ListView({ specs }: { specs: Spec[] }) {
|
|
|
274
394
|
{spec.title || spec.specName}
|
|
275
395
|
</CardTitle>
|
|
276
396
|
</Link>
|
|
397
|
+
{spec.title && spec.title !== spec.specName && (
|
|
398
|
+
<p className="text-sm text-muted-foreground mt-1">{spec.specName}</p>
|
|
399
|
+
)}
|
|
277
400
|
</div>
|
|
278
401
|
<div className="flex gap-2 shrink-0">
|
|
279
402
|
{spec.status && <StatusBadge status={spec.status} />}
|
|
@@ -281,15 +404,47 @@ function ListView({ specs }: { specs: Spec[] }) {
|
|
|
281
404
|
</div>
|
|
282
405
|
</div>
|
|
283
406
|
</CardHeader>
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
407
|
+
{/* Only render CardContent if there's metadata or tags to show */}
|
|
408
|
+
{((spec.updatedAt || hasSubSpecs || hasDependencies || (spec.tags && spec.tags.length > 0))) && (
|
|
409
|
+
<CardContent className="space-y-3">
|
|
410
|
+
{/* Metadata row */}
|
|
411
|
+
{(spec.updatedAt || hasSubSpecs || hasDependencies) && (
|
|
412
|
+
<div className="flex items-center gap-4 text-sm text-muted-foreground flex-wrap">
|
|
413
|
+
{spec.updatedAt && (
|
|
414
|
+
<div className="flex items-center gap-1.5">
|
|
415
|
+
<Clock className="h-3.5 w-3.5" />
|
|
416
|
+
<span>Updated {formatRelativeTime(spec.updatedAt)}</span>
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
{hasSubSpecs && (
|
|
420
|
+
<div className="flex items-center gap-1.5">
|
|
421
|
+
<FileText className="h-3.5 w-3.5" />
|
|
422
|
+
<span>+{spec.subSpecsCount} files</span>
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
{hasDependencies && (
|
|
426
|
+
<div className="flex items-center gap-1.5">
|
|
427
|
+
<GitBranch className="h-3.5 w-3.5" />
|
|
428
|
+
<span>
|
|
429
|
+
{spec.relationships!.dependsOn.length > 0 && `${spec.relationships!.dependsOn.length} deps`}
|
|
430
|
+
{spec.relationships!.dependsOn.length > 0 && spec.relationships!.related.length > 0 && ', '}
|
|
431
|
+
{spec.relationships!.related.length > 0 && `${spec.relationships!.related.length} related`}
|
|
432
|
+
</span>
|
|
433
|
+
</div>
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
)}
|
|
437
|
+
|
|
438
|
+
{/* Tags */}
|
|
439
|
+
{spec.tags && spec.tags.length > 0 && (
|
|
440
|
+
<div className="flex flex-wrap gap-2">
|
|
441
|
+
{spec.tags.map(tag => (
|
|
442
|
+
<Badge key={tag} variant="secondary" className="text-xs">
|
|
443
|
+
{tag}
|
|
444
|
+
</Badge>
|
|
445
|
+
))}
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
293
448
|
</CardContent>
|
|
294
449
|
)}
|
|
295
450
|
</Card>
|
|
@@ -299,63 +454,128 @@ function ListView({ specs }: { specs: Spec[] }) {
|
|
|
299
454
|
);
|
|
300
455
|
}
|
|
301
456
|
|
|
302
|
-
|
|
457
|
+
interface BoardViewProps {
|
|
458
|
+
specs: Spec[];
|
|
459
|
+
onStatusChange: (spec: Spec, status: SpecStatus) => void;
|
|
460
|
+
pendingSpecIds: Record<string, boolean>;
|
|
461
|
+
showArchived: boolean;
|
|
462
|
+
onToggleArchived: () => void;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function BoardView({ specs, onStatusChange, pendingSpecIds, showArchived, onToggleArchived }: BoardViewProps) {
|
|
466
|
+
const [draggingId, setDraggingId] = useState<string | null>(null);
|
|
467
|
+
const [activeDropZone, setActiveDropZone] = useState<SpecStatus | null>(null);
|
|
468
|
+
|
|
303
469
|
const columns = useMemo(() => {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
icon: Clock,
|
|
307
|
-
title: 'Planned',
|
|
308
|
-
colorClass: 'text-blue-600 dark:text-blue-400',
|
|
309
|
-
bgClass: 'bg-blue-50 dark:bg-blue-900/20',
|
|
310
|
-
borderClass: 'border-blue-200 dark:border-blue-800'
|
|
311
|
-
},
|
|
312
|
-
'in-progress': {
|
|
313
|
-
icon: PlayCircle,
|
|
314
|
-
title: 'In Progress',
|
|
315
|
-
colorClass: 'text-orange-600 dark:text-orange-400',
|
|
316
|
-
bgClass: 'bg-orange-50 dark:bg-orange-900/20',
|
|
317
|
-
borderClass: 'border-orange-200 dark:border-orange-800'
|
|
318
|
-
},
|
|
319
|
-
'complete': {
|
|
320
|
-
icon: CheckCircle2,
|
|
321
|
-
title: 'Complete',
|
|
322
|
-
colorClass: 'text-green-600 dark:text-green-400',
|
|
323
|
-
bgClass: 'bg-green-50 dark:bg-green-900/20',
|
|
324
|
-
borderClass: 'border-green-200 dark:border-green-800'
|
|
325
|
-
}
|
|
326
|
-
} as const;
|
|
327
|
-
|
|
328
|
-
const statuses = ['planned', 'in-progress', 'complete'] as const;
|
|
329
|
-
|
|
330
|
-
return statuses.map(status => ({
|
|
470
|
+
// Always show all columns, including archived (it will be rendered as collapsed bar when showArchived=false)
|
|
471
|
+
return BOARD_STATUSES.map(status => ({
|
|
331
472
|
status,
|
|
332
|
-
config:
|
|
473
|
+
config: STATUS_CONFIG[status],
|
|
333
474
|
specs: specs.filter(spec => spec.status === status),
|
|
334
475
|
}));
|
|
335
476
|
}, [specs]);
|
|
336
477
|
|
|
478
|
+
const specLookup = useMemo(() => {
|
|
479
|
+
const map = new Map<string, Spec>();
|
|
480
|
+
specs.forEach(spec => map.set(spec.id, spec));
|
|
481
|
+
return map;
|
|
482
|
+
}, [specs]);
|
|
483
|
+
|
|
484
|
+
const handleDragStart = useCallback((specId: string, event: DragEvent<HTMLDivElement>) => {
|
|
485
|
+
event.dataTransfer.setData('text/plain', specId);
|
|
486
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
487
|
+
setDraggingId(specId);
|
|
488
|
+
}, []);
|
|
489
|
+
|
|
490
|
+
const handleDragEnd = useCallback(() => {
|
|
491
|
+
setDraggingId(null);
|
|
492
|
+
setActiveDropZone(null);
|
|
493
|
+
}, []);
|
|
494
|
+
|
|
495
|
+
const handleDragOver = useCallback((status: SpecStatus, event: DragEvent<HTMLDivElement>) => {
|
|
496
|
+
if (!draggingId) return;
|
|
497
|
+
event.preventDefault();
|
|
498
|
+
event.dataTransfer.dropEffect = 'move';
|
|
499
|
+
setActiveDropZone(status);
|
|
500
|
+
}, [draggingId]);
|
|
501
|
+
|
|
502
|
+
const handleDragLeave = useCallback((status: SpecStatus, event: DragEvent<HTMLDivElement>) => {
|
|
503
|
+
if (!draggingId) return;
|
|
504
|
+
const related = event.relatedTarget as Node | null;
|
|
505
|
+
if (!related || !event.currentTarget.contains(related)) {
|
|
506
|
+
setActiveDropZone((current) => (current === status ? null : current));
|
|
507
|
+
}
|
|
508
|
+
}, [draggingId]);
|
|
509
|
+
|
|
510
|
+
const handleDrop = useCallback((status: SpecStatus, event: DragEvent<HTMLDivElement>) => {
|
|
511
|
+
event.preventDefault();
|
|
512
|
+
const draggedId = event.dataTransfer.getData('text/plain') || draggingId;
|
|
513
|
+
if (!draggedId) {
|
|
514
|
+
handleDragEnd();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const spec = specLookup.get(draggedId);
|
|
519
|
+
if (spec && spec.status !== status) {
|
|
520
|
+
onStatusChange(spec, status);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
handleDragEnd();
|
|
524
|
+
}, [draggingId, handleDragEnd, onStatusChange, specLookup]);
|
|
525
|
+
|
|
337
526
|
return (
|
|
338
|
-
<div className="
|
|
527
|
+
<div className="flex gap-6">
|
|
339
528
|
{columns.map(column => {
|
|
340
529
|
const Icon = column.config.icon;
|
|
530
|
+
const isArchivedColumn = column.status === 'archived';
|
|
531
|
+
|
|
341
532
|
return (
|
|
342
|
-
<div key={column.status} className=
|
|
533
|
+
<div key={column.status} className={cn(
|
|
534
|
+
"flex flex-col",
|
|
535
|
+
isArchivedColumn && !showArchived && "w-20 flex-shrink-0"
|
|
536
|
+
)}>
|
|
343
537
|
<div className={cn(
|
|
344
|
-
|
|
538
|
+
'sticky top-14 z-40 mb-4 rounded-lg border-2 bg-background transition-all',
|
|
345
539
|
column.config.bgClass,
|
|
346
|
-
column.config.borderClass
|
|
347
|
-
|
|
540
|
+
column.config.borderClass,
|
|
541
|
+
isArchivedColumn ? 'cursor-pointer hover:opacity-80' : '',
|
|
542
|
+
isArchivedColumn && !showArchived ? 'py-6 px-2' : 'p-3'
|
|
543
|
+
)}
|
|
544
|
+
onClick={isArchivedColumn ? onToggleArchived : undefined}
|
|
545
|
+
>
|
|
348
546
|
<h2 className={cn(
|
|
349
|
-
|
|
350
|
-
column.config.colorClass
|
|
547
|
+
'text-lg font-semibold flex items-center gap-2',
|
|
548
|
+
column.config.colorClass,
|
|
549
|
+
isArchivedColumn && !showArchived && 'flex-col text-sm gap-3'
|
|
351
550
|
)}>
|
|
352
551
|
<Icon className="h-5 w-5" />
|
|
353
|
-
{
|
|
354
|
-
|
|
552
|
+
{isArchivedColumn && !showArchived ? (
|
|
553
|
+
<>
|
|
554
|
+
<span className="vertical-text text-sm whitespace-nowrap">
|
|
555
|
+
{column.config.title}
|
|
556
|
+
</span>
|
|
557
|
+
<Badge variant="outline" className="text-xs">{column.specs.length}</Badge>
|
|
558
|
+
</>
|
|
559
|
+
) : (
|
|
560
|
+
<>
|
|
561
|
+
{column.config.title}
|
|
562
|
+
<Badge variant="outline" className="ml-auto">{column.specs.length}</Badge>
|
|
563
|
+
</>
|
|
564
|
+
)}
|
|
355
565
|
</h2>
|
|
356
566
|
</div>
|
|
357
567
|
|
|
358
|
-
|
|
568
|
+
{(!isArchivedColumn || showArchived) && (
|
|
569
|
+
<div
|
|
570
|
+
className={cn(
|
|
571
|
+
'space-y-3 flex-1 rounded-xl border border-transparent p-1 transition-colors overflow-y-auto max-h-[calc(100vh-250px)]',
|
|
572
|
+
draggingId && 'border-dashed border-muted-foreground/40',
|
|
573
|
+
draggingId && activeDropZone === column.status && 'bg-muted/40 border-primary/50'
|
|
574
|
+
)}
|
|
575
|
+
onDragOver={(event) => handleDragOver(column.status, event)}
|
|
576
|
+
onDragLeave={(event) => handleDragLeave(column.status, event)}
|
|
577
|
+
onDrop={(event) => handleDrop(column.status, event)}
|
|
578
|
+
>
|
|
359
579
|
{column.specs.map(spec => {
|
|
360
580
|
const priorityColors = {
|
|
361
581
|
'critical': 'border-l-red-500',
|
|
@@ -364,16 +584,33 @@ function BoardView({ specs }: { specs: Spec[] }) {
|
|
|
364
584
|
'low': 'border-l-gray-400'
|
|
365
585
|
};
|
|
366
586
|
const borderColor = priorityColors[spec.priority as keyof typeof priorityColors] || 'border-l-gray-300';
|
|
587
|
+
const isUpdating = Boolean(pendingSpecIds[spec.id]);
|
|
367
588
|
|
|
368
589
|
return (
|
|
369
|
-
<Card
|
|
370
|
-
key={spec.id}
|
|
590
|
+
<Card
|
|
591
|
+
key={spec.id}
|
|
592
|
+
draggable={!isUpdating}
|
|
593
|
+
onDragStart={(event) => {
|
|
594
|
+
if (isUpdating) {
|
|
595
|
+
event.preventDefault();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
handleDragStart(spec.id, event);
|
|
599
|
+
}}
|
|
600
|
+
onDragEnd={handleDragEnd}
|
|
601
|
+
aria-disabled={isUpdating}
|
|
371
602
|
className={cn(
|
|
372
|
-
|
|
373
|
-
borderColor
|
|
603
|
+
'relative hover:shadow-lg transition-all duration-150 hover:scale-[1.02] border-l-4 cursor-pointer',
|
|
604
|
+
borderColor,
|
|
605
|
+
isUpdating && 'opacity-60 cursor-wait'
|
|
374
606
|
)}
|
|
375
607
|
onClick={() => window.location.href = `/specs/${spec.specNumber || spec.id}`}
|
|
376
608
|
>
|
|
609
|
+
{isUpdating && (
|
|
610
|
+
<div className="absolute inset-0 rounded-lg bg-background/80 flex items-center justify-center text-xs font-medium">
|
|
611
|
+
Updating...
|
|
612
|
+
</div>
|
|
613
|
+
)}
|
|
377
614
|
<CardHeader className="pb-3">
|
|
378
615
|
<Link href={`/specs/${spec.specNumber || spec.id}`}>
|
|
379
616
|
<CardTitle className="text-sm font-medium hover:text-primary transition-colors">
|
|
@@ -408,7 +645,17 @@ function BoardView({ specs }: { specs: Spec[] }) {
|
|
|
408
645
|
</Card>
|
|
409
646
|
);
|
|
410
647
|
})}
|
|
648
|
+
|
|
649
|
+
{column.specs.length === 0 && (
|
|
650
|
+
<Card className="border-dashed border-gray-300 dark:border-gray-700 bg-transparent">
|
|
651
|
+
<CardContent className="py-8 text-center">
|
|
652
|
+
<Icon className={cn('mx-auto h-8 w-8 mb-2', column.config.colorClass, 'opacity-50')} />
|
|
653
|
+
<p className="text-sm text-muted-foreground">Drop here to move specs</p>
|
|
654
|
+
</CardContent>
|
|
655
|
+
</Card>
|
|
656
|
+
)}
|
|
411
657
|
</div>
|
|
658
|
+
)}
|
|
412
659
|
</div>
|
|
413
660
|
);
|
|
414
661
|
})}
|
|
@@ -136,6 +136,29 @@ export async function getSpecsWithSubSpecCount(projectId?: string): Promise<(Par
|
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Get all specs with sub-spec count and relationships (for comprehensive list view)
|
|
141
|
+
*/
|
|
142
|
+
export async function getSpecsWithMetadata(projectId?: string): Promise<(ParsedSpec & { subSpecsCount: number; relationships: SpecRelationships })[]> {
|
|
143
|
+
const specs = await specsService.getAllSpecs(projectId);
|
|
144
|
+
|
|
145
|
+
// Only count sub-specs and relationships for filesystem mode
|
|
146
|
+
if (projectId) {
|
|
147
|
+
return specs.map(spec => ({
|
|
148
|
+
...parseSpecTags(spec),
|
|
149
|
+
subSpecsCount: 0,
|
|
150
|
+
relationships: { dependsOn: [], related: [] }
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return specs.map(spec => {
|
|
155
|
+
const specDirPath = buildSpecDirPath(spec.filePath);
|
|
156
|
+
const subSpecsCount = countSubSpecs(specDirPath);
|
|
157
|
+
const relationships = getFilesystemRelationships(specDirPath);
|
|
158
|
+
return { ...parseSpecTags(spec), subSpecsCount, relationships };
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
139
162
|
/**
|
|
140
163
|
* Get a spec by ID (number or UUID)
|
|
141
164
|
*/
|