@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.
- package/.next/standalone/packages/ui/.next/BUILD_ID +1 -1
- package/.next/standalone/packages/ui/.next/build-manifest.json +2 -2
- package/.next/standalone/packages/ui/.next/prerender-manifest.json +3 -3
- package/.next/standalone/packages/ui/.next/server/app/_global-error.html +2 -2
- package/.next/standalone/packages/ui/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/packages/ui/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/packages/ui/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/packages/ui/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page.js.nft.json +1 -1
- package/.next/standalone/packages/ui/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/specs/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/app/stats/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__1d0c2012._.js +1 -1
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__b2fe773d._.js +3 -0
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/_e1889307._.js +1 -1
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/packages_ui_src_app_specs_specs-client_tsx_0bb8f8f8._.js +1 -1
- package/.next/standalone/packages/ui/.next/server/pages/404.html +2 -2
- package/.next/standalone/packages/ui/.next/server/pages/500.html +2 -2
- package/.next/standalone/packages/ui/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/packages/ui/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/packages/ui/.next/static/chunks/093ea4b175adb770.js +1 -0
- package/.next/standalone/packages/ui/.next/static/chunks/9a80c22382ddcfaf.css +1 -0
- package/.next/standalone/packages/ui/.next/static/chunks/{ae50cdf3322e1d02.js → c3d4d07de959ecf1.js} +1 -1
- package/.next/standalone/packages/ui/.next/static/chunks/c61cbb6a0ba3b6ab.js +1 -0
- package/.next/standalone/packages/ui/package.json +1 -1
- package/.next/standalone/packages/ui/src/app/globals.css +27 -0
- package/.next/standalone/packages/ui/src/app/specs/specs-client.tsx +249 -221
- package/.next/standalone/packages/ui/src/components/spec-detail-client.tsx +71 -27
- package/.next/standalone/packages/ui/src/components/table-of-contents.tsx +56 -37
- package/.next/standalone/packages/ui/tsconfig.tsbuildinfo +1 -1
- package/.next/static/chunks/093ea4b175adb770.js +1 -0
- package/.next/static/chunks/9a80c22382ddcfaf.css +1 -0
- package/.next/static/chunks/{ae50cdf3322e1d02.js → c3d4d07de959ecf1.js} +1 -1
- package/.next/static/chunks/c61cbb6a0ba3b6ab.js +1 -0
- package/package.json +1 -1
- package/.next/standalone/packages/ui/.next/server/chunks/ssr/[root-of-the-server]__9d8d3fd6._.js +0 -3
- package/.next/standalone/packages/ui/.next/static/chunks/061e3819fd59154d.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/148ab58e68b383da.js +0 -1
- package/.next/standalone/packages/ui/.next/static/chunks/9b54fc05b02c39e6.css +0 -1
- package/.next/static/chunks/061e3819fd59154d.js +0 -1
- package/.next/static/chunks/148ab58e68b383da.js +0 -1
- package/.next/static/chunks/9b54fc05b02c39e6.css +0 -1
- /package/.next/standalone/packages/ui/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_buildManifest.js +0 -0
- /package/.next/standalone/packages/ui/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_clientMiddlewareManifest.json +0 -0
- /package/.next/standalone/packages/ui/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_ssgManifest.js +0 -0
- /package/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_buildManifest.js +0 -0
- /package/.next/static/{18365Lfcsz3xqXCT_JLBo → lzZ2FuzU8N9IHbav88tsY}/_clientMiddlewareManifest.json +0 -0
- /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="
|
|
251
|
-
<div className=
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
<
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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-
|
|
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
|
|
311
|
-
<SelectItem value="id-asc">Oldest First
|
|
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
|
|
348
|
-
{
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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 ?
|
|
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-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
{(spec.updatedAt || hasSubSpecs || hasDependencies)
|
|
412
|
-
|
|
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
|
-
|
|
452
|
+
</>
|
|
453
|
+
) : (
|
|
454
|
+
<span className="invisible">No metadata</span> /* Keep height consistent */
|
|
436
455
|
)}
|
|
456
|
+
</div>
|
|
437
457
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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-
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
<
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
</
|
|
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
|
);
|