@leanspec/ui 0.2.5-dev.20251119025751 → 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 -1
- package/.next/standalone/packages/ui/.next/build-manifest.json +4 -5
- package/.next/standalone/packages/ui/.next/prerender-manifest.json +3 -28
- package/.next/standalone/packages/ui/.next/routes-manifest.json +8 -6
- package/.next/standalone/packages/ui/.next/server/app/_global-error/page/build-manifest.json +2 -3
- 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/page_client-reference-manifest.js +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/build-manifest.json +2 -3
- package/.next/standalone/packages/ui/.next/server/app/_not-found/page.js +3 -3
- 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.js +7 -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/build-manifest.json +2 -3
- package/.next/standalone/packages/ui/.next/server/app/page.js +3 -3
- 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/build-manifest.json +2 -3
- package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page.js +3 -3
- 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/build-manifest.json +2 -3
- package/.next/standalone/packages/ui/.next/server/app/specs/page.js +3 -3
- 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/build-manifest.json +2 -3
- package/.next/standalone/packages/ui/.next/server/app/stats/page.js +3 -3
- 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 -1
- 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]__d169fe70._.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]__f4ea2112._.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]__3fffdda5._.js → [root-of-the-server]__41f5b5c0._.js} +2 -2
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/{[root-of-the-server]__eefaabd3._.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]__e5891e5a._.js → [root-of-the-server]__cbbbfb5d._.js} +2 -2
- 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]__ff03fc1e._.js +7 -0
- 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/node_modules__pnpm_9710705b._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_app_loading_tsx_9304a706._.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/middleware-build-manifest.js +2 -3
- 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/c0fe4c94c7282ac0.js +4 -0
- 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/.next/static/chunks/turbopack-9cc79aa1b34ffcbe.js +3 -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 +352 -113
- 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/c0fe4c94c7282ac0.js +4 -0
- 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/.next/static/chunks/turbopack-9cc79aa1b34ffcbe.js +3 -0
- package/package.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/board/page/app-paths-manifest.json +0 -3
- package/.next/standalone/packages/ui/.next/server/app/board/page/build-manifest.json +0 -18
- package/.next/standalone/packages/ui/.next/server/app/board/page/next-font-manifest.json +0 -6
- package/.next/standalone/packages/ui/.next/server/app/board/page/react-loadable-manifest.json +0 -1
- package/.next/standalone/packages/ui/.next/server/app/board/page.js +0 -20
- package/.next/standalone/packages/ui/.next/server/app/board/page.js.nft.json +0 -1
- package/.next/standalone/packages/ui/.next/server/app/board/page_client-reference-manifest.js +0 -2
- package/.next/standalone/packages/ui/.next/server/app/board.html +0 -1
- package/.next/standalone/packages/ui/.next/server/app/board.meta +0 -14
- package/.next/standalone/packages/ui/.next/server/app/board.rsc +0 -41
- package/.next/standalone/packages/ui/.next/server/app/board.segments/_full.segment.rsc +0 -41
- package/.next/standalone/packages/ui/.next/server/app/board.segments/_index.segment.rsc +0 -26
- package/.next/standalone/packages/ui/.next/server/app/board.segments/_tree.segment.rsc +0 -9
- package/.next/standalone/packages/ui/.next/server/app/board.segments/board/__PAGE__.segment.rsc +0 -9
- package/.next/standalone/packages/ui/.next/server/app/board.segments/board.segment.rsc +0 -8
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__003ee184._.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__21119265._.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__6ec5cafb._.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__7acaa6ec._.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/ssr/6e9bd_next_dist_7af75658._.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__4b343655._.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]__b161f033._.js +0 -7
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__b50fa24c._.js +0 -7
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/_4f2c4868._.js +0 -4
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/_64a39e38._.js +0 -3
- 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__next-internal_server_app_board_page_actions_c5a3cb3c.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_app_board_loading_tsx_03dfd70d._.js +0 -3
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_app_board_page_tsx_9415dc46._.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/server/chunks/ssr/packages_ui_src_d725c10c._.js +0 -3
- package/.next/standalone/packages/ui/.next/static/chunks/107411d1a2f4b31d.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/17ac52d909def83e.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/212881511f73fc12.js +0 -5
- package/.next/standalone/packages/ui/.next/static/chunks/5057c3e1e7e13ca3.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/5c2072ad938de8ed.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/5d646f493d4c6928.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/88f695d32910a4ac.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/c210559fdfe60fef.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/df1731c03abf1aee.css +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/e237e00fd3a84178.js +0 -3
- package/.next/standalone/packages/ui/.next/static/chunks/ebd89051637b9a47.js +0 -4
- package/.next/standalone/packages/ui/.next/static/chunks/turbopack-7450632b40b2e378.js +0 -3
- package/.next/standalone/packages/ui/src/app/board/board-client.tsx +0 -162
- package/.next/standalone/packages/ui/src/app/board/loading.tsx +0 -43
- package/.next/standalone/packages/ui/src/app/board/page.tsx +0 -18
- package/.next/static/chunks/107411d1a2f4b31d.js +0 -1
- package/.next/static/chunks/17ac52d909def83e.js +0 -1
- package/.next/static/chunks/212881511f73fc12.js +0 -5
- package/.next/static/chunks/5057c3e1e7e13ca3.js +0 -1
- package/.next/static/chunks/5c2072ad938de8ed.js +0 -1
- package/.next/static/chunks/5d646f493d4c6928.js +0 -1
- package/.next/static/chunks/88f695d32910a4ac.js +0 -1
- package/.next/static/chunks/c210559fdfe60fef.js +0 -1
- package/.next/static/chunks/df1731c03abf1aee.css +0 -1
- package/.next/static/chunks/e237e00fd3a84178.js +0 -3
- package/.next/static/chunks/ebd89051637b9a47.js +0 -4
- package/.next/static/chunks/turbopack-7450632b40b2e378.js +0 -3
- /package/.next/standalone/packages/ui/.next/server/app/{board/page → api/specs/[id]/status/route}/server-reference-manifest.json +0 -0
- /package/.next/standalone/packages/ui/.next/server/app/{board/page.js.map → api/specs/[id]/status/route.js.map} +0 -0
- /package/.next/standalone/packages/ui/.next/static/{Wuxh-xmlQh_NqvIbobfYH → 18365Lfcsz3xqXCT_JLBo}/_buildManifest.js +0 -0
- /package/.next/standalone/packages/ui/.next/static/{Wuxh-xmlQh_NqvIbobfYH → 18365Lfcsz3xqXCT_JLBo}/_clientMiddlewareManifest.json +0 -0
- /package/.next/standalone/packages/ui/.next/static/{Wuxh-xmlQh_NqvIbobfYH → 18365Lfcsz3xqXCT_JLBo}/_ssgManifest.js +0 -0
- /package/.next/static/{Wuxh-xmlQh_NqvIbobfYH → 18365Lfcsz3xqXCT_JLBo}/_buildManifest.js +0 -0
- /package/.next/static/{Wuxh-xmlQh_NqvIbobfYH → 18365Lfcsz3xqXCT_JLBo}/_clientMiddlewareManifest.json +0 -0
- /package/.next/static/{Wuxh-xmlQh_NqvIbobfYH → 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';
|
|
@@ -14,28 +15,80 @@ import {
|
|
|
14
15
|
SelectTrigger,
|
|
15
16
|
SelectValue
|
|
16
17
|
} from '@/components/ui/select';
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
18
19
|
Search,
|
|
19
20
|
CheckCircle2,
|
|
20
21
|
PlayCircle,
|
|
21
22
|
Clock,
|
|
22
23
|
Archive,
|
|
23
24
|
LayoutGrid,
|
|
24
|
-
List as ListIcon
|
|
25
|
+
List as ListIcon,
|
|
26
|
+
FileText,
|
|
27
|
+
GitBranch
|
|
25
28
|
} from 'lucide-react';
|
|
26
29
|
import { StatusBadge } from '@/components/status-badge';
|
|
27
30
|
import { PriorityBadge } from '@/components/priority-badge';
|
|
28
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
|
+
}
|
|
29
80
|
|
|
30
81
|
interface Spec {
|
|
31
82
|
id: string;
|
|
32
83
|
specNumber: number | null;
|
|
33
84
|
specName: string;
|
|
34
85
|
title: string | null;
|
|
35
|
-
status:
|
|
86
|
+
status: SpecStatus | null;
|
|
36
87
|
priority: string | null;
|
|
37
88
|
tags: string[] | null;
|
|
38
89
|
updatedAt: Date | null;
|
|
90
|
+
subSpecsCount?: number;
|
|
91
|
+
relationships?: SpecRelationships;
|
|
39
92
|
}
|
|
40
93
|
|
|
41
94
|
interface Stats {
|
|
@@ -56,10 +109,13 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
|
|
|
56
109
|
const searchParams = useSearchParams();
|
|
57
110
|
const router = useRouter();
|
|
58
111
|
|
|
112
|
+
const [specs, setSpecs] = useState<Spec[]>(initialSpecs);
|
|
113
|
+
const [pendingSpecIds, setPendingSpecIds] = useState<Record<string, boolean>>({});
|
|
59
114
|
const [searchQuery, setSearchQuery] = useState('');
|
|
60
|
-
const [statusFilter, setStatusFilter] = useState<
|
|
115
|
+
const [statusFilter, setStatusFilter] = useState<'all' | SpecStatus>('all');
|
|
61
116
|
const [priorityFilter, setPriorityFilter] = useState<string>('all');
|
|
62
117
|
const [sortBy, setSortBy] = useState<SortBy>('id-desc');
|
|
118
|
+
const [showArchivedBoard, setShowArchivedBoard] = useState(false); // Start collapsed
|
|
63
119
|
const [viewMode, setViewMode] = useState<ViewMode>(() => {
|
|
64
120
|
// Initialize from URL or localStorage
|
|
65
121
|
const urlView = searchParams.get('view');
|
|
@@ -74,6 +130,55 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
|
|
|
74
130
|
|
|
75
131
|
const isFirstRender = useRef(true);
|
|
76
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
|
+
|
|
77
182
|
// Update URL when view mode changes (skip on initial mount)
|
|
78
183
|
useEffect(() => {
|
|
79
184
|
if (isFirstRender.current) {
|
|
@@ -98,35 +203,39 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
|
|
|
98
203
|
}, [viewMode, router]);
|
|
99
204
|
|
|
100
205
|
const filteredAndSortedSpecs = useMemo(() => {
|
|
101
|
-
const
|
|
206
|
+
const filtered = specs.filter(spec => {
|
|
102
207
|
const matchesSearch = !searchQuery ||
|
|
103
208
|
spec.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
104
209
|
spec.specName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
105
210
|
spec.tags?.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
106
211
|
|
|
107
|
-
const matchesStatus = statusFilter === 'all'
|
|
212
|
+
const matchesStatus = statusFilter === 'all'
|
|
213
|
+
? (viewMode === 'list' ? spec.status !== 'archived' : true)
|
|
214
|
+
: spec.status === statusFilter;
|
|
108
215
|
const matchesPriority = priorityFilter === 'all' || spec.priority === priorityFilter;
|
|
109
216
|
|
|
110
217
|
return matchesSearch && matchesStatus && matchesPriority;
|
|
111
218
|
});
|
|
112
219
|
|
|
220
|
+
const sorted = [...filtered];
|
|
221
|
+
|
|
113
222
|
// Sort
|
|
114
223
|
switch (sortBy) {
|
|
115
224
|
case 'id-desc':
|
|
116
|
-
|
|
225
|
+
sorted.sort((a, b) => (b.specNumber || 0) - (a.specNumber || 0));
|
|
117
226
|
break;
|
|
118
227
|
case 'id-asc':
|
|
119
|
-
|
|
228
|
+
sorted.sort((a, b) => (a.specNumber || 0) - (b.specNumber || 0));
|
|
120
229
|
break;
|
|
121
230
|
case 'updated-desc':
|
|
122
|
-
|
|
231
|
+
sorted.sort((a, b) => {
|
|
123
232
|
if (!a.updatedAt) return 1;
|
|
124
233
|
if (!b.updatedAt) return -1;
|
|
125
234
|
return b.updatedAt.getTime() - a.updatedAt.getTime();
|
|
126
235
|
});
|
|
127
236
|
break;
|
|
128
237
|
case 'title-asc':
|
|
129
|
-
|
|
238
|
+
sorted.sort((a, b) => {
|
|
130
239
|
const titleA = (a.title || a.specName).toLowerCase();
|
|
131
240
|
const titleB = (b.title || b.specName).toLowerCase();
|
|
132
241
|
return titleA.localeCompare(titleB);
|
|
@@ -134,8 +243,8 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
|
|
|
134
243
|
break;
|
|
135
244
|
}
|
|
136
245
|
|
|
137
|
-
return
|
|
138
|
-
}, [
|
|
246
|
+
return sorted;
|
|
247
|
+
}, [specs, searchQuery, statusFilter, priorityFilter, sortBy, viewMode]);
|
|
139
248
|
|
|
140
249
|
return (
|
|
141
250
|
<div className="min-h-screen bg-background p-8">
|
|
@@ -143,7 +252,7 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
|
|
|
143
252
|
<div className="mb-8">
|
|
144
253
|
<h1 className="text-4xl font-bold tracking-tight">Specifications</h1>
|
|
145
254
|
<p className="text-muted-foreground mt-2">
|
|
146
|
-
{viewMode === 'board' ? 'Kanban board view' : 'Browse all specifications'}
|
|
255
|
+
{viewMode === 'board' ? 'Kanban board view (active statuses only)' : 'Browse all specifications'}
|
|
147
256
|
</p>
|
|
148
257
|
</div>
|
|
149
258
|
|
|
@@ -162,7 +271,7 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
|
|
|
162
271
|
</div>
|
|
163
272
|
|
|
164
273
|
{/* Status Filter */}
|
|
165
|
-
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
274
|
+
<Select value={statusFilter} onValueChange={(value) => setStatusFilter(value as SpecStatus | 'all')}>
|
|
166
275
|
<SelectTrigger className="w-full sm:w-[180px]">
|
|
167
276
|
<SelectValue placeholder="Status" />
|
|
168
277
|
</SelectTrigger>
|
|
@@ -190,54 +299,62 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
|
|
|
190
299
|
</Select>
|
|
191
300
|
</div>
|
|
192
301
|
|
|
193
|
-
<div className="flex flex-col sm:flex-row justify-between gap-4">
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
<
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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>
|
|
316
|
+
|
|
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>
|
|
206
338
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
size="sm"
|
|
212
|
-
onClick={() => setViewMode('list')}
|
|
213
|
-
className="flex items-center gap-2"
|
|
214
|
-
>
|
|
215
|
-
<ListIcon className="h-4 w-4" />
|
|
216
|
-
List
|
|
217
|
-
</Button>
|
|
218
|
-
<Button
|
|
219
|
-
variant={viewMode === 'board' ? 'default' : 'outline'}
|
|
220
|
-
size="sm"
|
|
221
|
-
onClick={() => setViewMode('board')}
|
|
222
|
-
className="flex items-center gap-2"
|
|
223
|
-
>
|
|
224
|
-
<LayoutGrid className="h-4 w-4" />
|
|
225
|
-
Board
|
|
226
|
-
</Button>
|
|
339
|
+
{/* Results count */}
|
|
340
|
+
<div className="text-sm text-muted-foreground">
|
|
341
|
+
Showing {filteredAndSortedSpecs.length} of {specs.length} specs
|
|
342
|
+
</div>
|
|
227
343
|
</div>
|
|
228
344
|
</div>
|
|
229
345
|
</div>
|
|
230
346
|
|
|
231
|
-
{/* Results count */}
|
|
232
|
-
<div className="text-sm text-muted-foreground mb-4">
|
|
233
|
-
Showing {filteredAndSortedSpecs.length} of {initialSpecs.length} specs
|
|
234
|
-
</div>
|
|
235
|
-
|
|
236
347
|
{/* Content based on view mode */}
|
|
237
348
|
{viewMode === 'list' ? (
|
|
238
349
|
<ListView specs={filteredAndSortedSpecs} />
|
|
239
350
|
) : (
|
|
240
|
-
<BoardView
|
|
351
|
+
<BoardView
|
|
352
|
+
specs={filteredAndSortedSpecs}
|
|
353
|
+
onStatusChange={handleStatusChange}
|
|
354
|
+
pendingSpecIds={pendingSpecIds}
|
|
355
|
+
showArchived={showArchivedBoard}
|
|
356
|
+
onToggleArchived={() => setShowArchivedBoard(!showArchivedBoard)}
|
|
357
|
+
/>
|
|
241
358
|
)}
|
|
242
359
|
</div>
|
|
243
360
|
</div>
|
|
@@ -255,6 +372,8 @@ function ListView({ specs }: { specs: Spec[] }) {
|
|
|
255
372
|
'low': 'border-l-gray-400'
|
|
256
373
|
};
|
|
257
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);
|
|
258
377
|
|
|
259
378
|
return (
|
|
260
379
|
<Card
|
|
@@ -275,6 +394,9 @@ function ListView({ specs }: { specs: Spec[] }) {
|
|
|
275
394
|
{spec.title || spec.specName}
|
|
276
395
|
</CardTitle>
|
|
277
396
|
</Link>
|
|
397
|
+
{spec.title && spec.title !== spec.specName && (
|
|
398
|
+
<p className="text-sm text-muted-foreground mt-1">{spec.specName}</p>
|
|
399
|
+
)}
|
|
278
400
|
</div>
|
|
279
401
|
<div className="flex gap-2 shrink-0">
|
|
280
402
|
{spec.status && <StatusBadge status={spec.status} />}
|
|
@@ -282,15 +404,47 @@ function ListView({ specs }: { specs: Spec[] }) {
|
|
|
282
404
|
</div>
|
|
283
405
|
</div>
|
|
284
406
|
</CardHeader>
|
|
285
|
-
{
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
+
)}
|
|
294
448
|
</CardContent>
|
|
295
449
|
)}
|
|
296
450
|
</Card>
|
|
@@ -300,70 +454,128 @@ function ListView({ specs }: { specs: Spec[] }) {
|
|
|
300
454
|
);
|
|
301
455
|
}
|
|
302
456
|
|
|
303
|
-
|
|
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
|
+
|
|
304
469
|
const columns = useMemo(() => {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
icon: Clock,
|
|
308
|
-
title: 'Planned',
|
|
309
|
-
colorClass: 'text-blue-600 dark:text-blue-400',
|
|
310
|
-
bgClass: 'bg-blue-50 dark:bg-blue-900/20',
|
|
311
|
-
borderClass: 'border-blue-200 dark:border-blue-800'
|
|
312
|
-
},
|
|
313
|
-
'in-progress': {
|
|
314
|
-
icon: PlayCircle,
|
|
315
|
-
title: 'In Progress',
|
|
316
|
-
colorClass: 'text-orange-600 dark:text-orange-400',
|
|
317
|
-
bgClass: 'bg-orange-50 dark:bg-orange-900/20',
|
|
318
|
-
borderClass: 'border-orange-200 dark:border-orange-800'
|
|
319
|
-
},
|
|
320
|
-
'complete': {
|
|
321
|
-
icon: CheckCircle2,
|
|
322
|
-
title: 'Complete',
|
|
323
|
-
colorClass: 'text-green-600 dark:text-green-400',
|
|
324
|
-
bgClass: 'bg-green-50 dark:bg-green-900/20',
|
|
325
|
-
borderClass: 'border-green-200 dark:border-green-800'
|
|
326
|
-
},
|
|
327
|
-
'archived': {
|
|
328
|
-
icon: Archive,
|
|
329
|
-
title: 'Archived',
|
|
330
|
-
colorClass: 'text-gray-600 dark:text-gray-400',
|
|
331
|
-
bgClass: 'bg-gray-50 dark:bg-gray-900/20',
|
|
332
|
-
borderClass: 'border-gray-200 dark:border-gray-800'
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
const statuses = ['planned', 'in-progress', 'complete', 'archived'] as const;
|
|
337
|
-
|
|
338
|
-
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 => ({
|
|
339
472
|
status,
|
|
340
|
-
config:
|
|
473
|
+
config: STATUS_CONFIG[status],
|
|
341
474
|
specs: specs.filter(spec => spec.status === status),
|
|
342
475
|
}));
|
|
343
476
|
}, [specs]);
|
|
344
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
|
+
|
|
345
526
|
return (
|
|
346
|
-
<div className="
|
|
527
|
+
<div className="flex gap-6">
|
|
347
528
|
{columns.map(column => {
|
|
348
529
|
const Icon = column.config.icon;
|
|
530
|
+
const isArchivedColumn = column.status === 'archived';
|
|
531
|
+
|
|
349
532
|
return (
|
|
350
|
-
<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
|
+
)}>
|
|
351
537
|
<div className={cn(
|
|
352
|
-
|
|
538
|
+
'sticky top-14 z-40 mb-4 rounded-lg border-2 bg-background transition-all',
|
|
353
539
|
column.config.bgClass,
|
|
354
|
-
column.config.borderClass
|
|
355
|
-
|
|
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
|
+
>
|
|
356
546
|
<h2 className={cn(
|
|
357
|
-
|
|
358
|
-
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'
|
|
359
550
|
)}>
|
|
360
551
|
<Icon className="h-5 w-5" />
|
|
361
|
-
{
|
|
362
|
-
|
|
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
|
+
)}
|
|
363
565
|
</h2>
|
|
364
566
|
</div>
|
|
365
567
|
|
|
366
|
-
|
|
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
|
+
>
|
|
367
579
|
{column.specs.map(spec => {
|
|
368
580
|
const priorityColors = {
|
|
369
581
|
'critical': 'border-l-red-500',
|
|
@@ -372,16 +584,33 @@ function BoardView({ specs }: { specs: Spec[] }) {
|
|
|
372
584
|
'low': 'border-l-gray-400'
|
|
373
585
|
};
|
|
374
586
|
const borderColor = priorityColors[spec.priority as keyof typeof priorityColors] || 'border-l-gray-300';
|
|
587
|
+
const isUpdating = Boolean(pendingSpecIds[spec.id]);
|
|
375
588
|
|
|
376
589
|
return (
|
|
377
|
-
<Card
|
|
378
|
-
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}
|
|
379
602
|
className={cn(
|
|
380
|
-
|
|
381
|
-
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'
|
|
382
606
|
)}
|
|
383
607
|
onClick={() => window.location.href = `/specs/${spec.specNumber || spec.id}`}
|
|
384
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
|
+
)}
|
|
385
614
|
<CardHeader className="pb-3">
|
|
386
615
|
<Link href={`/specs/${spec.specNumber || spec.id}`}>
|
|
387
616
|
<CardTitle className="text-sm font-medium hover:text-primary transition-colors">
|
|
@@ -416,7 +645,17 @@ function BoardView({ specs }: { specs: Spec[] }) {
|
|
|
416
645
|
</Card>
|
|
417
646
|
);
|
|
418
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
|
+
)}
|
|
419
657
|
</div>
|
|
658
|
+
)}
|
|
420
659
|
</div>
|
|
421
660
|
);
|
|
422
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
|
*/
|