@leanspec/ui 0.2.5-dev.20251119062438 → 0.2.5-dev.20251120022746

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 (58) hide show
  1. package/.next/standalone/packages/ui/.next/BUILD_ID +1 -1
  2. package/.next/standalone/packages/ui/.next/build-manifest.json +2 -2
  3. package/.next/standalone/packages/ui/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/packages/ui/.next/server/app/_global-error.html +2 -2
  5. package/.next/standalone/packages/ui/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  9. package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/standalone/packages/ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/packages/ui/.next/server/app/_not-found.html +2 -2
  12. package/.next/standalone/packages/ui/.next/server/app/_not-found.rsc +2 -2
  13. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  14. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  15. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  16. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  17. package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/packages/ui/.next/server/app/page_client-reference-manifest.js +1 -1
  19. package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page.js.nft.json +1 -1
  20. package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
  21. package/.next/standalone/packages/ui/.next/server/app/specs/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/packages/ui/.next/server/app/stats/page_client-reference-manifest.js +1 -1
  23. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__1d0c2012._.js +1 -1
  24. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__b2fe773d._.js +3 -0
  25. package/.next/standalone/packages/ui/.next/server/chunks/ssr/_e1889307._.js +1 -1
  26. package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_app_specs_specs-client_tsx_0bb8f8f8._.js +1 -1
  27. package/.next/standalone/packages/ui/.next/server/pages/404.html +2 -2
  28. package/.next/standalone/packages/ui/.next/server/pages/500.html +2 -2
  29. package/.next/standalone/packages/ui/.next/server/server-reference-manifest.js +1 -1
  30. package/.next/standalone/packages/ui/.next/server/server-reference-manifest.json +1 -1
  31. package/.next/standalone/packages/ui/.next/static/chunks/093ea4b175adb770.js +1 -0
  32. package/.next/standalone/packages/ui/.next/static/chunks/9a80c22382ddcfaf.css +1 -0
  33. package/.next/standalone/packages/ui/.next/static/chunks/{ae50cdf3322e1d02.js → c3d4d07de959ecf1.js} +1 -1
  34. package/.next/standalone/packages/ui/.next/static/chunks/c61cbb6a0ba3b6ab.js +1 -0
  35. package/.next/standalone/packages/ui/package.json +1 -1
  36. package/.next/standalone/packages/ui/src/app/globals.css +27 -0
  37. package/.next/standalone/packages/ui/src/app/specs/specs-client.tsx +249 -221
  38. package/.next/standalone/packages/ui/src/components/spec-detail-client.tsx +71 -27
  39. package/.next/standalone/packages/ui/src/components/table-of-contents.tsx +56 -37
  40. package/.next/standalone/packages/ui/tsconfig.tsbuildinfo +1 -1
  41. package/.next/static/chunks/093ea4b175adb770.js +1 -0
  42. package/.next/static/chunks/9a80c22382ddcfaf.css +1 -0
  43. package/.next/static/chunks/{ae50cdf3322e1d02.js → c3d4d07de959ecf1.js} +1 -1
  44. package/.next/static/chunks/c61cbb6a0ba3b6ab.js +1 -0
  45. package/package.json +1 -1
  46. package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__9d8d3fd6._.js +0 -3
  47. package/.next/standalone/packages/ui/.next/static/chunks/061e3819fd59154d.js +0 -1
  48. package/.next/standalone/packages/ui/.next/static/chunks/148ab58e68b383da.js +0 -1
  49. package/.next/standalone/packages/ui/.next/static/chunks/9b54fc05b02c39e6.css +0 -1
  50. package/.next/static/chunks/061e3819fd59154d.js +0 -1
  51. package/.next/static/chunks/148ab58e68b383da.js +0 -1
  52. package/.next/static/chunks/9b54fc05b02c39e6.css +0 -1
  53. /package/.next/standalone/packages/ui/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_buildManifest.js +0 -0
  54. /package/.next/standalone/packages/ui/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_clientMiddlewareManifest.json +0 -0
  55. /package/.next/standalone/packages/ui/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_ssgManifest.js +0 -0
  56. /package/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_buildManifest.js +0 -0
  57. /package/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_clientMiddlewareManifest.json +0 -0
  58. /package/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_ssgManifest.js +0 -0
@@ -16,15 +16,17 @@ import {
16
16
  SelectValue
17
17
  } from '@/components/ui/select';
18
18
  import {
19
- Search,
20
- CheckCircle2,
21
- PlayCircle,
19
+ Search,
20
+ CheckCircle2,
21
+ PlayCircle,
22
22
  Clock,
23
23
  Archive,
24
24
  LayoutGrid,
25
25
  List as ListIcon,
26
26
  FileText,
27
- GitBranch
27
+ GitBranch,
28
+ Maximize2,
29
+ Minimize2
28
30
  } from 'lucide-react';
29
31
  import { StatusBadge } from '@/components/status-badge';
30
32
  import { PriorityBadge } from '@/components/priority-badge';
@@ -108,7 +110,7 @@ type SortBy = 'id-desc' | 'id-asc' | 'updated-desc' | 'title-asc';
108
110
  export function SpecsClient({ initialSpecs }: SpecsClientProps) {
109
111
  const searchParams = useSearchParams();
110
112
  const router = useRouter();
111
-
113
+
112
114
  const [specs, setSpecs] = useState<Spec[]>(initialSpecs);
113
115
  const [pendingSpecIds, setPendingSpecIds] = useState<Record<string, boolean>>({});
114
116
  const [searchQuery, setSearchQuery] = useState('');
@@ -116,18 +118,19 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
116
118
  const [priorityFilter, setPriorityFilter] = useState<string>('all');
117
119
  const [sortBy, setSortBy] = useState<SortBy>('id-desc');
118
120
  const [showArchivedBoard, setShowArchivedBoard] = useState(false); // Start collapsed
121
+ const [isWideMode, setIsWideMode] = useState(false);
119
122
  const [viewMode, setViewMode] = useState<ViewMode>(() => {
120
123
  // Initialize from URL or localStorage
121
124
  const urlView = searchParams.get('view');
122
125
  if (urlView === 'board' || urlView === 'list') return urlView;
123
-
126
+
124
127
  if (typeof window !== 'undefined') {
125
128
  const stored = localStorage.getItem('specs-view-mode');
126
129
  if (stored === 'board' || stored === 'list') return stored;
127
130
  }
128
131
  return 'list';
129
132
  });
130
-
133
+
131
134
  const isFirstRender = useRef(true);
132
135
 
133
136
  useEffect(() => {
@@ -185,7 +188,7 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
185
188
  isFirstRender.current = false;
186
189
  return;
187
190
  }
188
-
191
+
189
192
  const current = new URLSearchParams(window.location.search);
190
193
  if (viewMode === 'board') {
191
194
  current.set('view', 'board');
@@ -195,7 +198,7 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
195
198
  const search = current.toString();
196
199
  const query = search ? `?${search}` : '';
197
200
  router.replace(`/specs${query}`, { scroll: false });
198
-
201
+
199
202
  // Persist to localStorage
200
203
  if (typeof window !== 'undefined') {
201
204
  localStorage.setItem('specs-view-mode', viewMode);
@@ -209,7 +212,7 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
209
212
  spec.specName.toLowerCase().includes(searchQuery.toLowerCase()) ||
210
213
  spec.tags?.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
211
214
 
212
- const matchesStatus = statusFilter === 'all'
215
+ const matchesStatus = statusFilter === 'all'
213
216
  ? (viewMode === 'list' ? spec.status !== 'archived' : true)
214
217
  : spec.status === statusFilter;
215
218
  const matchesPriority = priorityFilter === 'all' || spec.priority === priorityFilter;
@@ -242,120 +245,131 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
242
245
  });
243
246
  break;
244
247
  }
245
-
246
248
  return sorted;
247
249
  }, [specs, searchQuery, statusFilter, priorityFilter, sortBy, viewMode]);
248
250
 
249
251
  return (
250
- <div className="min-h-screen bg-background p-8">
251
- <div className="max-w-7xl mx-auto">
252
- <div className="mb-8">
253
- <h1 className="text-4xl font-bold tracking-tight">Specifications</h1>
254
- <p className="text-muted-foreground mt-2">
255
- {viewMode === 'board' ? 'Kanban board view (active statuses only)' : 'Browse all specifications'}
256
- </p>
257
- </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>
258
267
 
259
- {/* Filters and View Switcher */}
260
- <div className="flex flex-col gap-4 mb-6">
261
- <div className="flex flex-col sm:flex-row gap-4">
262
- {/* Search */}
263
- <div className="flex-1 relative">
264
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
265
- <Input
266
- placeholder="Search specs..."
267
- value={searchQuery}
268
- onChange={(e) => setSearchQuery(e.target.value)}
269
- className="pl-10"
270
- />
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>
289
+
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>
271
300
  </div>
272
301
 
273
- {/* Status Filter */}
274
- <Select value={statusFilter} onValueChange={(value) => setStatusFilter(value as SpecStatus | 'all')}>
275
- <SelectTrigger className="w-full sm:w-[180px]">
276
- <SelectValue placeholder="Status" />
277
- </SelectTrigger>
278
- <SelectContent>
279
- <SelectItem value="all">All Status</SelectItem>
280
- <SelectItem value="planned">Planned</SelectItem>
281
- <SelectItem value="in-progress">In Progress</SelectItem>
282
- <SelectItem value="complete">Complete</SelectItem>
283
- <SelectItem value="archived">Archived</SelectItem>
284
- </SelectContent>
285
- </Select>
286
-
287
- {/* Priority Filter */}
288
- <Select value={priorityFilter} onValueChange={setPriorityFilter}>
289
- <SelectTrigger className="w-full sm:w-[180px]">
290
- <SelectValue placeholder="Priority" />
291
- </SelectTrigger>
292
- <SelectContent>
293
- <SelectItem value="all">All Priority</SelectItem>
294
- <SelectItem value="critical">Critical</SelectItem>
295
- <SelectItem value="high">High</SelectItem>
296
- <SelectItem value="medium">Medium</SelectItem>
297
- <SelectItem value="low">Low</SelectItem>
298
- </SelectContent>
299
- </Select>
300
- </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>
312
+
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>
301
338
 
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
339
  <Select value={sortBy} onValueChange={(v) => setSortBy(v as SortBy)}>
306
- <SelectTrigger className="w-full sm:w-[220px]">
340
+ <SelectTrigger className="w-[180px] h-9">
307
341
  <SelectValue placeholder="Sort by" />
308
342
  </SelectTrigger>
309
343
  <SelectContent>
310
- <SelectItem value="id-desc">Newest First (ID ↓)</SelectItem>
311
- <SelectItem value="id-asc">Oldest First (ID ↑)</SelectItem>
344
+ <SelectItem value="id-desc">Newest First</SelectItem>
345
+ <SelectItem value="id-asc">Oldest First</SelectItem>
312
346
  <SelectItem value="updated-desc">Recently Updated</SelectItem>
313
347
  <SelectItem value="title-asc">Title (A-Z)</SelectItem>
314
348
  </SelectContent>
315
349
  </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>
338
-
339
- {/* Results count */}
340
- <div className="text-sm text-muted-foreground">
341
- Showing {filteredAndSortedSpecs.length} of {specs.length} specs
342
- </div>
343
350
  </div>
344
351
  </div>
345
352
  </div>
346
353
 
347
- {/* Content based on view mode */}
348
- {viewMode === 'list' ? (
349
- <ListView specs={filteredAndSortedSpecs} />
350
- ) : (
351
- <BoardView
352
- specs={filteredAndSortedSpecs}
353
- onStatusChange={handleStatusChange}
354
- pendingSpecIds={pendingSpecIds}
355
- showArchived={showArchivedBoard}
356
- onToggleArchived={() => setShowArchivedBoard(!showArchivedBoard)}
357
- />
358
- )}
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
+ )}
372
+ </div>
359
373
  </div>
360
374
  </div>
361
375
  );
@@ -363,7 +377,7 @@ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
363
377
 
364
378
  function ListView({ specs }: { specs: Spec[] }) {
365
379
  return (
366
- <div className="grid grid-cols-1 gap-4">
380
+ <div className="grid grid-cols-1 gap-4 pb-8">
367
381
  {specs.map(spec => {
368
382
  const priorityColors = {
369
383
  'critical': 'border-l-red-500',
@@ -376,8 +390,8 @@ function ListView({ specs }: { specs: Spec[] }) {
376
390
  const hasSubSpecs = !!(spec.subSpecsCount && spec.subSpecsCount > 0);
377
391
 
378
392
  return (
379
- <Card
380
- key={spec.id}
393
+ <Card
394
+ key={spec.id}
381
395
  className={cn(
382
396
  "hover:shadow-lg transition-all duration-150 hover:scale-[1.01] border-l-4 cursor-pointer",
383
397
  borderColor
@@ -388,14 +402,17 @@ function ListView({ specs }: { specs: Spec[] }) {
388
402
  <div className="flex items-start justify-between gap-4">
389
403
  <div className="flex-1 min-w-0">
390
404
  <Link href={`/specs/${spec.specNumber || spec.id}`}>
391
- <CardTitle className="text-lg font-semibold hover:text-primary transition-colors">
392
- {spec.specNumber ? `#${spec.specNumber.toString().padStart(3, '0')}` : spec.specName}
393
- {' '}
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}
394
411
  {spec.title || spec.specName}
395
412
  </CardTitle>
396
413
  </Link>
397
414
  {spec.title && spec.title !== spec.specName && (
398
- <p className="text-sm text-muted-foreground mt-1">{spec.specName}</p>
415
+ <p className="text-xs font-mono text-muted-foreground mt-1.5 truncate">{spec.specName}</p>
399
416
  )}
400
417
  </div>
401
418
  <div className="flex gap-2 shrink-0">
@@ -404,12 +421,12 @@ function ListView({ specs }: { specs: Spec[] }) {
404
421
  </div>
405
422
  </div>
406
423
  </CardHeader>
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">
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
+ <>
413
430
  {spec.updatedAt && (
414
431
  <div className="flex items-center gap-1.5">
415
432
  <Clock className="h-3.5 w-3.5" />
@@ -432,21 +449,23 @@ function ListView({ specs }: { specs: Spec[] }) {
432
449
  </span>
433
450
  </div>
434
451
  )}
435
- </div>
452
+ </>
453
+ ) : (
454
+ <span className="invisible">No metadata</span> /* Keep height consistent */
436
455
  )}
456
+ </div>
437
457
 
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
- )}
448
- </CardContent>
449
- )}
458
+ {/* Tags (Right) */}
459
+ {spec.tags && spec.tags.length > 0 && (
460
+ <div className="flex flex-wrap gap-2 justify-end shrink-0">
461
+ {spec.tags.map(tag => (
462
+ <Badge key={tag} variant="outline" className="text-xs font-mono text-muted-foreground hover:text-foreground transition-colors">
463
+ {tag}
464
+ </Badge>
465
+ ))}
466
+ </div>
467
+ )}
468
+ </CardContent>
450
469
  </Card>
451
470
  );
452
471
  })}
@@ -524,24 +543,24 @@ function BoardView({ specs, onStatusChange, pendingSpecIds, showArchived, onTogg
524
543
  }, [draggingId, handleDragEnd, onStatusChange, specLookup]);
525
544
 
526
545
  return (
527
- <div className="flex gap-6">
546
+ <div className="flex gap-6 h-full pb-2">
528
547
  {columns.map(column => {
529
548
  const Icon = column.config.icon;
530
549
  const isArchivedColumn = column.status === 'archived';
531
-
550
+
532
551
  return (
533
552
  <div key={column.status} className={cn(
534
- "flex flex-col",
535
- isArchivedColumn && !showArchived && "w-20 flex-shrink-0"
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"
536
555
  )}>
537
556
  <div className={cn(
538
- 'sticky top-14 z-40 mb-4 rounded-lg border-2 bg-background transition-all',
557
+ 'flex-none mb-4 rounded-lg border-2 bg-background transition-all',
539
558
  column.config.bgClass,
540
559
  column.config.borderClass,
541
560
  isArchivedColumn ? 'cursor-pointer hover:opacity-80' : '',
542
561
  isArchivedColumn && !showArchived ? 'py-6 px-2' : 'p-3'
543
562
  )}
544
- onClick={isArchivedColumn ? onToggleArchived : undefined}
563
+ onClick={isArchivedColumn ? onToggleArchived : undefined}
545
564
  >
546
565
  <h2 className={cn(
547
566
  'text-lg font-semibold flex items-center gap-2',
@@ -567,94 +586,103 @@ function BoardView({ specs, onStatusChange, pendingSpecIds, showArchived, onTogg
567
586
 
568
587
  {(!isArchivedColumn || showArchived) && (
569
588
  <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
- >
579
- {column.specs.map(spec => {
580
- const priorityColors = {
581
- 'critical': 'border-l-red-500',
582
- 'high': 'border-l-orange-500',
583
- 'medium': 'border-l-blue-500',
584
- 'low': 'border-l-gray-400'
585
- };
586
- const borderColor = priorityColors[spec.priority as keyof typeof priorityColors] || 'border-l-gray-300';
587
- const isUpdating = Boolean(pendingSpecIds[spec.id]);
588
-
589
- return (
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}
602
- className={cn(
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'
606
- )}
607
- onClick={() => window.location.href = `/specs/${spec.specNumber || spec.id}`}
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
- )}
614
- <CardHeader className="pb-3">
615
- <Link href={`/specs/${spec.specNumber || spec.id}`}>
616
- <CardTitle className="text-sm font-medium hover:text-primary transition-colors">
617
- {spec.specNumber ? `#${spec.specNumber}` : spec.specName}
618
- </CardTitle>
619
- </Link>
620
- </CardHeader>
621
- <CardContent className="space-y-2">
622
- <p className="text-sm text-muted-foreground line-clamp-2">
623
- {spec.title || spec.specName}
624
- </p>
625
-
626
- <div className="flex items-center gap-2 flex-wrap">
627
- {spec.priority && <PriorityBadge priority={spec.priority} />}
628
-
629
- {spec.tags && spec.tags.length > 0 && (
630
- <>
631
- {spec.tags.slice(0, 2).map(tag => (
632
- <Badge key={tag} variant="outline" className="text-xs">
633
- {tag}
634
- </Badge>
635
- ))}
636
- {spec.tags.length > 2 && (
637
- <Badge variant="outline" className="text-xs">
638
- +{spec.tags.length - 2}
639
- </Badge>
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>
640
669
  )}
641
- </>
642
- )}
643
- </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>
644
682
  </CardContent>
645
683
  </Card>
646
- );
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
- )}
657
- </div>
684
+ )}
685
+ </div>
658
686
  )}
659
687
  </div>
660
688
  );