@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.
Files changed (200) hide show
  1. package/.next/standalone/node_modules/.pnpm/tiktoken@1.0.22/node_modules/tiktoken/tiktoken_bg.wasm +0 -0
  2. package/.next/standalone/packages/ui/.next/BUILD_ID +1 -1
  3. package/.next/standalone/packages/ui/.next/app-path-routes-manifest.json +1 -1
  4. package/.next/standalone/packages/ui/.next/build-manifest.json +4 -5
  5. package/.next/standalone/packages/ui/.next/prerender-manifest.json +3 -28
  6. package/.next/standalone/packages/ui/.next/routes-manifest.json +8 -6
  7. package/.next/standalone/packages/ui/.next/server/app/_global-error/page/build-manifest.json +2 -3
  8. package/.next/standalone/packages/ui/.next/server/app/_global-error/page.js +1 -1
  9. package/.next/standalone/packages/ui/.next/server/app/_global-error/page.js.nft.json +1 -1
  10. package/.next/standalone/packages/ui/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/packages/ui/.next/server/app/_global-error.html +2 -2
  12. package/.next/standalone/packages/ui/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  14. package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  16. package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  17. package/.next/standalone/packages/ui/.next/server/app/_not-found/page/build-manifest.json +2 -3
  18. package/.next/standalone/packages/ui/.next/server/app/_not-found/page.js +3 -3
  19. package/.next/standalone/packages/ui/.next/server/app/_not-found/page.js.nft.json +1 -1
  20. package/.next/standalone/packages/ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  21. package/.next/standalone/packages/ui/.next/server/app/_not-found.html +2 -2
  22. package/.next/standalone/packages/ui/.next/server/app/_not-found.rsc +16 -16
  23. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_full.segment.rsc +16 -16
  24. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  25. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
  26. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  27. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_tree.segment.rsc +5 -5
  28. package/.next/standalone/packages/ui/.next/server/app/api/projects/[id]/specs/route.js +1 -1
  29. package/.next/standalone/packages/ui/.next/server/app/api/projects/[id]/specs/route.js.nft.json +1 -1
  30. package/.next/standalone/packages/ui/.next/server/app/api/projects/route.js +1 -1
  31. package/.next/standalone/packages/ui/.next/server/app/api/projects/route.js.nft.json +1 -1
  32. package/.next/standalone/packages/ui/.next/server/app/api/revalidate/route.js +1 -1
  33. package/.next/standalone/packages/ui/.next/server/app/api/revalidate/route.js.nft.json +1 -1
  34. package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/dependency-graph/route.js +1 -1
  35. package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/dependency-graph/route.js.nft.json +1 -1
  36. package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/route.js +1 -1
  37. package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/route.js.nft.json +1 -1
  38. package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/status/route/app-paths-manifest.json +3 -0
  39. package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/status/route/build-manifest.json +11 -0
  40. package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/status/route.js +7 -0
  41. package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/status/route.js.nft.json +1 -0
  42. package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/status/route_client-reference-manifest.js +2 -0
  43. package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/subspecs/[file]/route.js +1 -1
  44. package/.next/standalone/packages/ui/.next/server/app/api/specs/[id]/subspecs/[file]/route.js.nft.json +1 -1
  45. package/.next/standalone/packages/ui/.next/server/app/api/stats/route.js +1 -1
  46. package/.next/standalone/packages/ui/.next/server/app/api/stats/route.js.nft.json +1 -1
  47. package/.next/standalone/packages/ui/.next/server/app/page/build-manifest.json +2 -3
  48. package/.next/standalone/packages/ui/.next/server/app/page.js +3 -3
  49. package/.next/standalone/packages/ui/.next/server/app/page.js.nft.json +1 -1
  50. package/.next/standalone/packages/ui/.next/server/app/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page/build-manifest.json +2 -3
  52. package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page.js +3 -3
  53. package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page.js.nft.json +1 -1
  54. package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
  55. package/.next/standalone/packages/ui/.next/server/app/specs/page/build-manifest.json +2 -3
  56. package/.next/standalone/packages/ui/.next/server/app/specs/page.js +3 -3
  57. package/.next/standalone/packages/ui/.next/server/app/specs/page.js.nft.json +1 -1
  58. package/.next/standalone/packages/ui/.next/server/app/specs/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/packages/ui/.next/server/app/stats/page/build-manifest.json +2 -3
  60. package/.next/standalone/packages/ui/.next/server/app/stats/page.js +3 -3
  61. package/.next/standalone/packages/ui/.next/server/app/stats/page.js.nft.json +1 -1
  62. package/.next/standalone/packages/ui/.next/server/app/stats/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/packages/ui/.next/server/app-paths-manifest.json +1 -1
  64. package/.next/standalone/packages/ui/.next/server/chunks/730ea_ui__next-internal_server_app_api_specs_[id]_status_route_actions_5d700407.js +3 -0
  65. package/.next/standalone/packages/ui/.next/server/chunks/{[root-of-the-server]__d169fe70._.js → [root-of-the-server]__175bef84._.js} +2 -2
  66. package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__3971eae5._.js +3 -0
  67. package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__6bca1621._.js +3 -0
  68. package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__87a3475a._.js +3 -0
  69. package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__9f0f4c0b._.js +3 -0
  70. package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__c1c9f5f5._.js +3 -0
  71. package/.next/standalone/packages/ui/.next/server/chunks/{[root-of-the-server]__f4ea2112._.js → [root-of-the-server]__c6689757._.js} +2 -2
  72. package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__cd1fb0a2._.js +4 -0
  73. package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__e2071b2e._.js +3 -0
  74. package/.next/standalone/packages/ui/.next/server/chunks/e0876_tiktoken_3efea2dc._.js +3 -0
  75. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__1d0c2012._.js +1 -1
  76. package/.next/standalone/packages/ui/.next/server/chunks/ssr/{[root-of-the-server]__c65aedd0._.js → [root-of-the-server]__299c81cc._.js} +2 -2
  77. package/.next/standalone/packages/ui/.next/server/chunks/ssr/{[root-of-the-server]__3fffdda5._.js → [root-of-the-server]__41f5b5c0._.js} +2 -2
  78. package/.next/standalone/packages/ui/.next/server/chunks/ssr/{[root-of-the-server]__eefaabd3._.js → [root-of-the-server]__5ca2e973._.js} +2 -2
  79. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__7f55abaa._.js +1 -1
  80. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__9d8d3fd6._.js +3 -0
  81. package/.next/standalone/packages/ui/.next/server/chunks/ssr/{[root-of-the-server]__e5891e5a._.js → [root-of-the-server]__cbbbfb5d._.js} +2 -2
  82. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__e79a982a._.js +7 -0
  83. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__ff03fc1e._.js +7 -0
  84. package/.next/standalone/packages/ui/.next/server/chunks/ssr/_000dd317._.js +1 -1
  85. package/.next/standalone/packages/ui/.next/server/chunks/ssr/_6cd9a5e0._.js +3 -0
  86. package/.next/standalone/packages/ui/.next/server/chunks/ssr/_b483c9fe._.js +3 -0
  87. package/.next/standalone/packages/ui/.next/server/chunks/ssr/_b5040d31._.js +5 -0
  88. package/.next/standalone/packages/ui/.next/server/chunks/ssr/_e1889307._.js +3 -0
  89. package/.next/standalone/packages/ui/.next/server/chunks/ssr/node_modules__pnpm_9710705b._.js +3 -0
  90. package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_app_loading_tsx_9304a706._.js +3 -0
  91. package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_app_specs_specs-client_tsx_0bb8f8f8._.js +3 -0
  92. package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_components_specs-nav-sidebar_tsx_8237ed13._.js +3 -0
  93. package/.next/standalone/packages/ui/.next/server/middleware-build-manifest.js +2 -3
  94. package/.next/standalone/packages/ui/.next/server/pages/404.html +2 -2
  95. package/.next/standalone/packages/ui/.next/server/pages/500.html +2 -2
  96. package/.next/standalone/packages/ui/.next/server/server-reference-manifest.js +1 -1
  97. package/.next/standalone/packages/ui/.next/server/server-reference-manifest.json +1 -1
  98. package/.next/standalone/packages/ui/.next/static/chunks/061e3819fd59154d.js +1 -0
  99. package/.next/standalone/packages/ui/.next/static/chunks/148ab58e68b383da.js +1 -0
  100. package/.next/standalone/packages/ui/.next/static/chunks/5f97d665a4d9dc62.js +3 -0
  101. package/.next/standalone/packages/ui/.next/static/chunks/9b54fc05b02c39e6.css +1 -0
  102. package/.next/standalone/packages/ui/.next/static/chunks/9b7b7024c39c5f99.js +1 -0
  103. package/.next/standalone/packages/ui/.next/static/chunks/{f2151fb88f8fc70a.js → ae50cdf3322e1d02.js} +1 -1
  104. package/.next/standalone/packages/ui/.next/static/chunks/c0fe4c94c7282ac0.js +4 -0
  105. package/.next/standalone/packages/ui/.next/static/chunks/d79bf953f0dfb650.js +1 -0
  106. package/.next/standalone/packages/ui/.next/static/chunks/e50fb8d0b728cd35.js +5 -0
  107. package/.next/standalone/packages/ui/.next/static/chunks/e6e238dbf1d3e740.js +1 -0
  108. package/.next/standalone/packages/ui/.next/static/chunks/turbopack-9cc79aa1b34ffcbe.js +3 -0
  109. package/.next/standalone/packages/ui/package.json +1 -1
  110. package/.next/standalone/packages/ui/src/app/api/specs/[id]/status/route.ts +122 -0
  111. package/.next/standalone/packages/ui/src/app/globals.css +10 -0
  112. package/.next/standalone/packages/ui/src/app/specs/page.tsx +2 -2
  113. package/.next/standalone/packages/ui/src/app/specs/specs-client.tsx +352 -113
  114. package/.next/standalone/packages/ui/src/lib/db/service-queries.ts +23 -0
  115. package/.next/standalone/packages/ui/src/lib/specs/sources/filesystem-source.ts +46 -6
  116. package/.next/standalone/packages/ui/tsconfig.json +2 -1
  117. package/.next/standalone/packages/ui/tsconfig.tsbuildinfo +1 -1
  118. package/.next/standalone/packages/ui/vitest.config.ts +1 -0
  119. package/.next/static/chunks/061e3819fd59154d.js +1 -0
  120. package/.next/static/chunks/148ab58e68b383da.js +1 -0
  121. package/.next/static/chunks/5f97d665a4d9dc62.js +3 -0
  122. package/.next/static/chunks/9b54fc05b02c39e6.css +1 -0
  123. package/.next/static/chunks/9b7b7024c39c5f99.js +1 -0
  124. package/.next/static/chunks/{f2151fb88f8fc70a.js → ae50cdf3322e1d02.js} +1 -1
  125. package/.next/static/chunks/c0fe4c94c7282ac0.js +4 -0
  126. package/.next/static/chunks/d79bf953f0dfb650.js +1 -0
  127. package/.next/static/chunks/e50fb8d0b728cd35.js +5 -0
  128. package/.next/static/chunks/e6e238dbf1d3e740.js +1 -0
  129. package/.next/static/chunks/turbopack-9cc79aa1b34ffcbe.js +3 -0
  130. package/package.json +1 -1
  131. package/.next/standalone/packages/ui/.next/server/app/board/page/app-paths-manifest.json +0 -3
  132. package/.next/standalone/packages/ui/.next/server/app/board/page/build-manifest.json +0 -18
  133. package/.next/standalone/packages/ui/.next/server/app/board/page/next-font-manifest.json +0 -6
  134. package/.next/standalone/packages/ui/.next/server/app/board/page/react-loadable-manifest.json +0 -1
  135. package/.next/standalone/packages/ui/.next/server/app/board/page.js +0 -20
  136. package/.next/standalone/packages/ui/.next/server/app/board/page.js.nft.json +0 -1
  137. package/.next/standalone/packages/ui/.next/server/app/board/page_client-reference-manifest.js +0 -2
  138. package/.next/standalone/packages/ui/.next/server/app/board.html +0 -1
  139. package/.next/standalone/packages/ui/.next/server/app/board.meta +0 -14
  140. package/.next/standalone/packages/ui/.next/server/app/board.rsc +0 -41
  141. package/.next/standalone/packages/ui/.next/server/app/board.segments/_full.segment.rsc +0 -41
  142. package/.next/standalone/packages/ui/.next/server/app/board.segments/_index.segment.rsc +0 -26
  143. package/.next/standalone/packages/ui/.next/server/app/board.segments/_tree.segment.rsc +0 -9
  144. package/.next/standalone/packages/ui/.next/server/app/board.segments/board/__PAGE__.segment.rsc +0 -9
  145. package/.next/standalone/packages/ui/.next/server/app/board.segments/board.segment.rsc +0 -8
  146. package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__003ee184._.js +0 -3
  147. package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__21119265._.js +0 -3
  148. package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__6ec5cafb._.js +0 -3
  149. package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__7acaa6ec._.js +0 -3
  150. package/.next/standalone/packages/ui/.next/server/chunks/[root-of-the-server]__a2c55ffb._.js +0 -3
  151. package/.next/standalone/packages/ui/.next/server/chunks/ssr/6e9bd_next_dist_7af75658._.js +0 -3
  152. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__4b343655._.js +0 -3
  153. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__68015eee._.js +0 -3
  154. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__b161f033._.js +0 -7
  155. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__b50fa24c._.js +0 -7
  156. package/.next/standalone/packages/ui/.next/server/chunks/ssr/_4f2c4868._.js +0 -4
  157. package/.next/standalone/packages/ui/.next/server/chunks/ssr/_64a39e38._.js +0 -3
  158. package/.next/standalone/packages/ui/.next/server/chunks/ssr/_a527620e._.js +0 -3
  159. package/.next/standalone/packages/ui/.next/server/chunks/ssr/_d7e4bd50._.js +0 -5
  160. package/.next/standalone/packages/ui/.next/server/chunks/ssr/_f8b2190a._.js +0 -3
  161. package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui__next-internal_server_app_board_page_actions_c5a3cb3c.js +0 -3
  162. package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_app_board_loading_tsx_03dfd70d._.js +0 -3
  163. package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_app_board_page_tsx_9415dc46._.js +0 -3
  164. package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_components_ui_input_tsx_190a7701._.js +0 -3
  165. package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_d725c10c._.js +0 -3
  166. package/.next/standalone/packages/ui/.next/static/chunks/107411d1a2f4b31d.js +0 -1
  167. package/.next/standalone/packages/ui/.next/static/chunks/17ac52d909def83e.js +0 -1
  168. package/.next/standalone/packages/ui/.next/static/chunks/212881511f73fc12.js +0 -5
  169. package/.next/standalone/packages/ui/.next/static/chunks/5057c3e1e7e13ca3.js +0 -1
  170. package/.next/standalone/packages/ui/.next/static/chunks/5c2072ad938de8ed.js +0 -1
  171. package/.next/standalone/packages/ui/.next/static/chunks/5d646f493d4c6928.js +0 -1
  172. package/.next/standalone/packages/ui/.next/static/chunks/88f695d32910a4ac.js +0 -1
  173. package/.next/standalone/packages/ui/.next/static/chunks/c210559fdfe60fef.js +0 -1
  174. package/.next/standalone/packages/ui/.next/static/chunks/df1731c03abf1aee.css +0 -1
  175. package/.next/standalone/packages/ui/.next/static/chunks/e237e00fd3a84178.js +0 -3
  176. package/.next/standalone/packages/ui/.next/static/chunks/ebd89051637b9a47.js +0 -4
  177. package/.next/standalone/packages/ui/.next/static/chunks/turbopack-7450632b40b2e378.js +0 -3
  178. package/.next/standalone/packages/ui/src/app/board/board-client.tsx +0 -162
  179. package/.next/standalone/packages/ui/src/app/board/loading.tsx +0 -43
  180. package/.next/standalone/packages/ui/src/app/board/page.tsx +0 -18
  181. package/.next/static/chunks/107411d1a2f4b31d.js +0 -1
  182. package/.next/static/chunks/17ac52d909def83e.js +0 -1
  183. package/.next/static/chunks/212881511f73fc12.js +0 -5
  184. package/.next/static/chunks/5057c3e1e7e13ca3.js +0 -1
  185. package/.next/static/chunks/5c2072ad938de8ed.js +0 -1
  186. package/.next/static/chunks/5d646f493d4c6928.js +0 -1
  187. package/.next/static/chunks/88f695d32910a4ac.js +0 -1
  188. package/.next/static/chunks/c210559fdfe60fef.js +0 -1
  189. package/.next/static/chunks/df1731c03abf1aee.css +0 -1
  190. package/.next/static/chunks/e237e00fd3a84178.js +0 -3
  191. package/.next/static/chunks/ebd89051637b9a47.js +0 -4
  192. package/.next/static/chunks/turbopack-7450632b40b2e378.js +0 -3
  193. /package/.next/standalone/packages/ui/.next/server/app/{board/page → api/specs/[id]/status/route}/server-reference-manifest.json +0 -0
  194. /package/.next/standalone/packages/ui/.next/server/app/{board/page.js.map → api/specs/[id]/status/route.js.map} +0 -0
  195. /package/.next/standalone/packages/ui/.next/static/{Wuxh-xmlQh_NqvIbobfYH → 18365Lfcsz3xqXCT_JLBo}/_buildManifest.js +0 -0
  196. /package/.next/standalone/packages/ui/.next/static/{Wuxh-xmlQh_NqvIbobfYH → 18365Lfcsz3xqXCT_JLBo}/_clientMiddlewareManifest.json +0 -0
  197. /package/.next/standalone/packages/ui/.next/static/{Wuxh-xmlQh_NqvIbobfYH → 18365Lfcsz3xqXCT_JLBo}/_ssgManifest.js +0 -0
  198. /package/.next/static/{Wuxh-xmlQh_NqvIbobfYH → 18365Lfcsz3xqXCT_JLBo}/_buildManifest.js +0 -0
  199. /package/.next/static/{Wuxh-xmlQh_NqvIbobfYH → 18365Lfcsz3xqXCT_JLBo}/_clientMiddlewareManifest.json +0 -0
  200. /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: string | null;
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<string>('all');
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 specs = initialSpecs.filter(spec => {
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' || spec.status === statusFilter;
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
- specs.sort((a, b) => (b.specNumber || 0) - (a.specNumber || 0));
225
+ sorted.sort((a, b) => (b.specNumber || 0) - (a.specNumber || 0));
117
226
  break;
118
227
  case 'id-asc':
119
- specs.sort((a, b) => (a.specNumber || 0) - (b.specNumber || 0));
228
+ sorted.sort((a, b) => (a.specNumber || 0) - (b.specNumber || 0));
120
229
  break;
121
230
  case 'updated-desc':
122
- specs.sort((a, b) => {
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
- specs.sort((a, b) => {
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 specs;
138
- }, [initialSpecs, searchQuery, statusFilter, priorityFilter, sortBy]);
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
- {/* Sort Controls */}
195
- <Select value={sortBy} onValueChange={(v) => setSortBy(v as SortBy)}>
196
- <SelectTrigger className="w-full sm:w-[220px]">
197
- <SelectValue placeholder="Sort by" />
198
- </SelectTrigger>
199
- <SelectContent>
200
- <SelectItem value="id-desc">Newest First (ID ↓)</SelectItem>
201
- <SelectItem value="id-asc">Oldest First (ID )</SelectItem>
202
- <SelectItem value="updated-desc">Recently Updated</SelectItem>
203
- <SelectItem value="title-asc">Title (A-Z)</SelectItem>
204
- </SelectContent>
205
- </Select>
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
- {/* View Mode Switcher */}
208
- <div className="flex gap-2">
209
- <Button
210
- variant={viewMode === 'list' ? 'default' : 'outline'}
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 specs={filteredAndSortedSpecs} />
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
- {spec.tags && spec.tags.length > 0 && (
286
- <CardContent>
287
- <div className="flex flex-wrap gap-2">
288
- {spec.tags.map(tag => (
289
- <Badge key={tag} variant="secondary">
290
- {tag}
291
- </Badge>
292
- ))}
293
- </div>
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
- function BoardView({ specs }: { specs: Spec[] }) {
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
- const statusConfig = {
306
- 'planned': {
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: statusConfig[status],
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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
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="flex flex-col">
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
- "sticky top-14 z-40 mb-4 p-3 rounded-lg border-2 bg-background",
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
- "text-lg font-semibold flex items-center gap-2",
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
- {column.config.title}
362
- <Badge variant="outline" className="ml-auto">{column.specs.length}</Badge>
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
- <div className="space-y-3 flex-1">
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
- "hover:shadow-lg transition-all duration-150 hover:scale-[1.02] border-l-4 cursor-pointer",
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
  */