@leanspec/ui 0.2.5-dev.20251119032109 → 0.2.5-dev.20251120015656

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