@leanspec/ui 0.2.3 → 0.2.4
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/dist/standalone/packages/web/.next/BUILD_ID +1 -1
- package/dist/standalone/packages/web/.next/build-manifest.json +2 -2
- package/dist/standalone/packages/web/.next/prerender-manifest.json +3 -3
- package/dist/standalone/packages/web/.next/server/app/_global-error.html +2 -2
- package/dist/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.html +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/projects/[id]/specs/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/revalidate/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/dependency-graph/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/subspecs/[file]/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/stats/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/board/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.html +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/board.segments/_full.segment.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/board.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/board/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/board.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/[id]/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/stats/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2e0f9179._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__577d6d08._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e54bc4b8._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f8978f3e._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__be46bb7c._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_7dedc302._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_ad71cd8c._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_c5a5c652._.js +1 -1
- package/dist/standalone/packages/web/.next/server/pages/404.html +2 -2
- package/dist/standalone/packages/web/.next/server/pages/500.html +2 -2
- package/dist/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
- package/dist/standalone/packages/web/.next/server/server-reference-manifest.json +1 -1
- package/dist/{static/chunks/0de258404bcae76f.js → standalone/packages/web/.next/static/chunks/8864b47e107cbe63.js} +1 -1
- package/dist/{static/chunks/09ff02250dd56621.js → standalone/packages/web/.next/static/chunks/a2889ecda42c83e7.js} +1 -1
- package/dist/standalone/packages/web/.next/static/chunks/c22619397bb8368e.js +1 -0
- package/dist/standalone/packages/web/components.json +20 -0
- package/dist/standalone/packages/web/drizzle/0000_reflective_thena.sql +59 -0
- package/dist/standalone/packages/web/drizzle/0001_fresh_carmella_unuscione.sql +1 -0
- package/dist/standalone/packages/web/drizzle/meta/0000_snapshot.json +427 -0
- package/dist/standalone/packages/web/drizzle/meta/0001_snapshot.json +436 -0
- package/dist/standalone/packages/web/drizzle/meta/_journal.json +20 -0
- package/dist/standalone/packages/web/drizzle.config.ts +10 -0
- package/dist/standalone/packages/web/eslint.config.mjs +18 -0
- package/dist/standalone/packages/web/next.config.ts +7 -0
- package/dist/standalone/packages/web/package.json +1 -1
- package/dist/standalone/packages/web/postcss.config.mjs +8 -0
- package/dist/standalone/packages/web/src/app/api/projects/[id]/specs/route.ts +23 -0
- package/dist/standalone/packages/web/src/app/api/projects/route.ts +19 -0
- package/dist/standalone/packages/web/src/app/api/revalidate/route.ts +63 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.test.ts +51 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.ts +171 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/route.ts +36 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/subspecs/[file]/route.ts +46 -0
- package/dist/standalone/packages/web/src/app/api/stats/route.ts +19 -0
- package/dist/standalone/packages/web/src/app/board/board-client.tsx +162 -0
- package/dist/standalone/packages/web/src/app/board/loading.tsx +43 -0
- package/dist/standalone/packages/web/src/app/board/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/dashboard-client.tsx +364 -0
- package/dist/standalone/packages/web/src/app/error.tsx +43 -0
- package/dist/standalone/packages/web/src/app/globals.css +531 -0
- package/dist/standalone/packages/web/src/app/home-client.tsx +277 -0
- package/dist/standalone/packages/web/src/app/layout.tsx +70 -0
- package/dist/standalone/packages/web/src/app/loading.tsx +87 -0
- package/dist/standalone/packages/web/src/app/not-found.tsx +27 -0
- package/dist/standalone/packages/web/src/app/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/specs/[id]/loading.tsx +5 -0
- package/dist/standalone/packages/web/src/app/specs/[id]/page.tsx +43 -0
- package/dist/standalone/packages/web/src/app/specs/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/specs/specs-client.tsx +425 -0
- package/dist/standalone/packages/web/src/app/stats/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/stats/stats-client.tsx +283 -0
- package/dist/standalone/packages/web/src/components/back-to-top.tsx +46 -0
- package/dist/standalone/packages/web/src/components/empty-state.tsx +52 -0
- package/dist/standalone/packages/web/src/components/main-sidebar.tsx +175 -0
- package/dist/standalone/packages/web/src/components/markdown-link.test.ts +96 -0
- package/dist/standalone/packages/web/src/components/markdown-link.tsx +95 -0
- package/dist/standalone/packages/web/src/components/navigation.tsx +210 -0
- package/dist/standalone/packages/web/src/components/priority-badge.tsx +53 -0
- package/dist/standalone/packages/web/src/components/quick-search.tsx +180 -0
- package/dist/standalone/packages/web/src/components/skeletons.tsx +119 -0
- package/dist/standalone/packages/web/src/components/spec-dependency-graph.tsx +369 -0
- package/dist/standalone/packages/web/src/components/spec-detail-client.tsx +372 -0
- package/dist/standalone/packages/web/src/components/spec-detail-loading-shell.tsx +42 -0
- package/dist/standalone/packages/web/src/components/spec-detail-wrapper.tsx +70 -0
- package/dist/standalone/packages/web/src/components/spec-metadata.tsx +136 -0
- package/dist/standalone/packages/web/src/components/spec-sidebar.tsx +127 -0
- package/dist/standalone/packages/web/src/components/spec-timeline.tsx +186 -0
- package/dist/standalone/packages/web/src/components/specs-nav-sidebar.tsx +561 -0
- package/dist/standalone/packages/web/src/components/status-badge.tsx +53 -0
- package/dist/standalone/packages/web/src/components/sub-spec-tabs.tsx +143 -0
- package/dist/standalone/packages/web/src/components/table-of-contents.tsx +130 -0
- package/dist/standalone/packages/web/src/components/theme-provider.tsx +11 -0
- package/dist/standalone/packages/web/src/components/theme-toggle.tsx +37 -0
- package/dist/standalone/packages/web/src/components/ui/avatar.tsx +50 -0
- package/dist/standalone/packages/web/src/components/ui/badge.tsx +36 -0
- package/dist/standalone/packages/web/src/components/ui/breadcrumb.tsx +110 -0
- package/dist/standalone/packages/web/src/components/ui/button.tsx +57 -0
- package/dist/standalone/packages/web/src/components/ui/card.tsx +76 -0
- package/dist/standalone/packages/web/src/components/ui/command.tsx +153 -0
- package/dist/standalone/packages/web/src/components/ui/dialog.tsx +122 -0
- package/dist/standalone/packages/web/src/components/ui/input.tsx +24 -0
- package/dist/standalone/packages/web/src/components/ui/select.tsx +159 -0
- package/dist/standalone/packages/web/src/components/ui/separator.tsx +31 -0
- package/dist/standalone/packages/web/src/components/ui/skeleton.tsx +15 -0
- package/dist/standalone/packages/web/src/components/ui/tabs.tsx +55 -0
- package/dist/standalone/packages/web/src/components/ui/toast.tsx +30 -0
- package/dist/standalone/packages/web/src/components/ui/tooltip.tsx +32 -0
- package/dist/standalone/packages/web/src/lib/date-utils.ts +76 -0
- package/dist/standalone/packages/web/src/lib/db/index.ts +42 -0
- package/dist/standalone/packages/web/src/lib/db/migrate.ts +18 -0
- package/dist/standalone/packages/web/src/lib/db/queries.ts +114 -0
- package/dist/standalone/packages/web/src/lib/db/schema.ts +123 -0
- package/dist/standalone/packages/web/src/lib/db/seed.ts +156 -0
- package/dist/standalone/packages/web/src/lib/db/service-queries.ts +216 -0
- package/dist/standalone/packages/web/src/lib/dependency-graph.ts +105 -0
- package/dist/standalone/packages/web/src/lib/specs/service.ts +120 -0
- package/dist/standalone/packages/web/src/lib/specs/sources/database-source.ts +94 -0
- package/dist/standalone/packages/web/src/lib/specs/sources/filesystem-source.ts +249 -0
- package/dist/standalone/packages/web/src/lib/specs/types.ts +55 -0
- package/dist/standalone/packages/web/src/lib/stores/specs-sidebar-store.ts +152 -0
- package/dist/standalone/packages/web/src/lib/sub-specs.ts +171 -0
- package/dist/standalone/packages/web/src/lib/utils.ts +17 -0
- package/dist/standalone/packages/web/src/types/specs.ts +18 -0
- package/dist/standalone/packages/web/tailwind.config.ts +58 -0
- package/dist/standalone/packages/web/tsconfig.json +34 -0
- package/dist/standalone/packages/web/vitest.config.ts +14 -0
- package/dist/standalone/specs/100-release-process-typecheck-failure/README.md +266 -0
- package/dist/standalone/specs/101-sidebar-scroll-position-drift/README.md +100 -0
- package/package.json +5 -3
- package/dist/BUILD_ID +0 -1
- package/dist/static/chunks/a3e649fcddd3d715.js +0 -1
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_buildManifest.js +0 -0
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_clientMiddlewareManifest.json +0 -0
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_ssgManifest.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/0c19c69aa7625475.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/116800b03245a1e5.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/19e80edf527aef5c.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/2ece90370908f56c.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/36fd2dddb486f6bc.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/5c2072ad938de8ed.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/6577fe797a336bab.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/6a05a93ec8fa7b83.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/7f732ea69e643219.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a02c1f50ff00204f.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a45464b9776dd88e.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a6dad97d9634a72d.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/ae04dcd433be6dab.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/b20313408e970968.css +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c46095e1a421d93f.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c48dd4c72d7c5ef4.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c557ac675be79771.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/dca0c854c59234cd.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/df1731c03abf1aee.css +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/dfd41488ad062cd5.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/ebd89051637b9a47.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/f3ec9fd77a8618b1.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/turbopack-7450632b40b2e378.js +0 -0
- /package/dist/{public → standalone/packages/web/public}/f864aa7e7061c0600e35cf3d879b27cf.txt +0 -0
- /package/dist/{public → standalone/packages/web/public}/favicon.ico +0 -0
- /package/dist/{public → standalone/packages/web/public}/file.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/github-mark-white.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/github-mark.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/globe.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/icon.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo-dark-bg.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo-with-bg.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/next.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/vercel.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/window.svg +0 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
6
|
+
import { Input } from '@/components/ui/input';
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue
|
|
13
|
+
} from '@/components/ui/select';
|
|
14
|
+
import { Search, FileText, CheckCircle2, PlayCircle, Clock } from 'lucide-react';
|
|
15
|
+
import { StatusBadge } from '@/components/status-badge';
|
|
16
|
+
import { PriorityBadge } from '@/components/priority-badge';
|
|
17
|
+
|
|
18
|
+
interface Spec {
|
|
19
|
+
id: string;
|
|
20
|
+
specNumber: number | null;
|
|
21
|
+
specName: string;
|
|
22
|
+
title: string | null;
|
|
23
|
+
status: string | null;
|
|
24
|
+
priority: string | null;
|
|
25
|
+
tags: string[] | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Stats {
|
|
29
|
+
totalSpecs: number;
|
|
30
|
+
completionRate: number;
|
|
31
|
+
specsByStatus: { status: string; count: number }[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface HomeClientProps {
|
|
35
|
+
initialProjects: unknown[];
|
|
36
|
+
initialStats: Stats;
|
|
37
|
+
initialSpecs: Spec[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function HomeClient({ initialProjects: _initialProjects, initialStats, initialSpecs }: HomeClientProps) {
|
|
41
|
+
void _initialProjects;
|
|
42
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
43
|
+
const [statusFilter, setStatusFilter] = useState<string>('all');
|
|
44
|
+
const [priorityFilter, setPriorityFilter] = useState<string>('all');
|
|
45
|
+
|
|
46
|
+
const filteredSpecs = useMemo(() => {
|
|
47
|
+
return initialSpecs.filter(spec => {
|
|
48
|
+
const matchesSearch = !searchQuery ||
|
|
49
|
+
spec.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
50
|
+
spec.specName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
51
|
+
spec.tags?.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
52
|
+
|
|
53
|
+
const matchesStatus = statusFilter === 'all' || spec.status === statusFilter;
|
|
54
|
+
const matchesPriority = priorityFilter === 'all' || spec.priority === priorityFilter;
|
|
55
|
+
|
|
56
|
+
return matchesSearch && matchesStatus && matchesPriority;
|
|
57
|
+
});
|
|
58
|
+
}, [initialSpecs, searchQuery, statusFilter, priorityFilter]);
|
|
59
|
+
|
|
60
|
+
const stats = initialStats;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="min-h-screen bg-background">
|
|
64
|
+
{/* Stats Section */}
|
|
65
|
+
<section className="container mx-auto px-4 py-8">
|
|
66
|
+
<div className="mb-8">
|
|
67
|
+
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight">LeanSpec Web</h1>
|
|
68
|
+
<p className="text-muted-foreground mt-2">Interactive spec showcase for AI-powered development</p>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
72
|
+
{/* Total Specs */}
|
|
73
|
+
<Card className="relative overflow-hidden">
|
|
74
|
+
<div className="absolute inset-0 bg-linear-to-br from-blue-500/10 to-transparent" />
|
|
75
|
+
<CardHeader className="relative pb-3">
|
|
76
|
+
<div className="flex items-center justify-between">
|
|
77
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">Total Specs</CardTitle>
|
|
78
|
+
<FileText className="h-5 w-5 text-blue-600" />
|
|
79
|
+
</div>
|
|
80
|
+
</CardHeader>
|
|
81
|
+
<CardContent className="relative">
|
|
82
|
+
<div className="text-3xl font-bold">{stats.totalSpecs}</div>
|
|
83
|
+
</CardContent>
|
|
84
|
+
</Card>
|
|
85
|
+
|
|
86
|
+
{/* Completed */}
|
|
87
|
+
<Card className="relative overflow-hidden">
|
|
88
|
+
<div className="absolute inset-0 bg-linear-to-br from-green-500/10 to-transparent" />
|
|
89
|
+
<CardHeader className="relative pb-3">
|
|
90
|
+
<div className="flex items-center justify-between">
|
|
91
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">Completed</CardTitle>
|
|
92
|
+
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
|
93
|
+
</div>
|
|
94
|
+
</CardHeader>
|
|
95
|
+
<CardContent className="relative">
|
|
96
|
+
<div className="text-3xl font-bold">
|
|
97
|
+
{stats.specsByStatus.find(s => s.status === 'complete')?.count || 0}
|
|
98
|
+
</div>
|
|
99
|
+
<p className="text-xs text-muted-foreground mt-1 flex items-center gap-1">
|
|
100
|
+
<span className="text-green-600">{stats.completionRate}%</span>
|
|
101
|
+
completion rate
|
|
102
|
+
</p>
|
|
103
|
+
</CardContent>
|
|
104
|
+
</Card>
|
|
105
|
+
|
|
106
|
+
{/* In Progress */}
|
|
107
|
+
<Card className="relative overflow-hidden">
|
|
108
|
+
<div className="absolute inset-0 bg-linear-to-br from-orange-500/10 to-transparent" />
|
|
109
|
+
<CardHeader className="relative pb-3">
|
|
110
|
+
<div className="flex items-center justify-between">
|
|
111
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">In Progress</CardTitle>
|
|
112
|
+
<PlayCircle className="h-5 w-5 text-orange-600" />
|
|
113
|
+
</div>
|
|
114
|
+
</CardHeader>
|
|
115
|
+
<CardContent className="relative">
|
|
116
|
+
<div className="text-3xl font-bold">
|
|
117
|
+
{stats.specsByStatus.find(s => s.status === 'in-progress')?.count || 0}
|
|
118
|
+
</div>
|
|
119
|
+
</CardContent>
|
|
120
|
+
</Card>
|
|
121
|
+
|
|
122
|
+
{/* Planned */}
|
|
123
|
+
<Card className="relative overflow-hidden">
|
|
124
|
+
<div className="absolute inset-0 bg-linear-to-br from-blue-500/10 to-transparent" />
|
|
125
|
+
<CardHeader className="relative pb-3">
|
|
126
|
+
<div className="flex items-center justify-between">
|
|
127
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">Planned</CardTitle>
|
|
128
|
+
<Clock className="h-5 w-5 text-blue-600" />
|
|
129
|
+
</div>
|
|
130
|
+
</CardHeader>
|
|
131
|
+
<CardContent className="relative">
|
|
132
|
+
<div className="text-3xl font-bold">
|
|
133
|
+
{stats.specsByStatus.find(s => s.status === 'planned')?.count || 0}
|
|
134
|
+
</div>
|
|
135
|
+
</CardContent>
|
|
136
|
+
</Card>
|
|
137
|
+
</div>
|
|
138
|
+
</section>
|
|
139
|
+
|
|
140
|
+
{/* Specs List */}
|
|
141
|
+
<section className="container mx-auto px-4 py-4">
|
|
142
|
+
<div className="mb-1">
|
|
143
|
+
<h2 className="text-xl sm:text-2xl font-bold mb-4">Specifications</h2>
|
|
144
|
+
|
|
145
|
+
{/* Search and Filters */}
|
|
146
|
+
<div className="lg:sticky lg:top-14 lg:z-40 bg-background pb-4 pt-2 mb-1">
|
|
147
|
+
<div className="flex flex-col gap-3 mb-3">
|
|
148
|
+
<div className="relative flex-1">
|
|
149
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
|
150
|
+
<Input
|
|
151
|
+
placeholder="Search specs..."
|
|
152
|
+
value={searchQuery}
|
|
153
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
154
|
+
className="pl-10"
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div className="flex gap-2 overflow-x-auto pb-2">
|
|
159
|
+
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
160
|
+
<SelectTrigger className="w-[140px] sm:w-[180px]">
|
|
161
|
+
<SelectValue placeholder="All Status" />
|
|
162
|
+
</SelectTrigger>
|
|
163
|
+
<SelectContent>
|
|
164
|
+
<SelectItem value="all">All Status</SelectItem>
|
|
165
|
+
<SelectItem value="planned">Planned</SelectItem>
|
|
166
|
+
<SelectItem value="in-progress">In Progress</SelectItem>
|
|
167
|
+
<SelectItem value="complete">Complete</SelectItem>
|
|
168
|
+
<SelectItem value="archived">Archived</SelectItem>
|
|
169
|
+
</SelectContent>
|
|
170
|
+
</Select>
|
|
171
|
+
|
|
172
|
+
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
|
|
173
|
+
<SelectTrigger className="w-[140px] sm:w-[180px]">
|
|
174
|
+
<SelectValue placeholder="All Priority" />
|
|
175
|
+
</SelectTrigger>
|
|
176
|
+
<SelectContent>
|
|
177
|
+
<SelectItem value="all">All Priority</SelectItem>
|
|
178
|
+
<SelectItem value="critical">Critical</SelectItem>
|
|
179
|
+
<SelectItem value="high">High</SelectItem>
|
|
180
|
+
<SelectItem value="medium">Medium</SelectItem>
|
|
181
|
+
<SelectItem value="low">Low</SelectItem>
|
|
182
|
+
</SelectContent>
|
|
183
|
+
</Select>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Desktop Table View */}
|
|
190
|
+
<div className="hidden md:block border rounded-lg border-gray-200 dark:border-gray-800 overflow-x-auto">
|
|
191
|
+
<table className="w-full">
|
|
192
|
+
<thead className="lg:sticky lg:top-14 lg:z-30 border-b backdrop-blur border-gray-200 bg-background/95 dark:border-gray-800">
|
|
193
|
+
<tr>
|
|
194
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
195
|
+
Spec
|
|
196
|
+
</th>
|
|
197
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
198
|
+
Title
|
|
199
|
+
</th>
|
|
200
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
201
|
+
Status
|
|
202
|
+
</th>
|
|
203
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
204
|
+
Priority
|
|
205
|
+
</th>
|
|
206
|
+
</tr>
|
|
207
|
+
</thead>
|
|
208
|
+
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
|
209
|
+
{filteredSpecs.map((spec) => (
|
|
210
|
+
<tr
|
|
211
|
+
key={spec.id}
|
|
212
|
+
className="group hover:bg-muted/50 transition-colors cursor-pointer"
|
|
213
|
+
onClick={() => window.location.href = `/specs/${spec.specNumber || spec.id}`}
|
|
214
|
+
>
|
|
215
|
+
<td className="px-6 py-4 whitespace-nowrap">
|
|
216
|
+
<span className="text-primary group-hover:underline font-medium">
|
|
217
|
+
{spec.specNumber ? `#${spec.specNumber}` : spec.specName}
|
|
218
|
+
</span>
|
|
219
|
+
</td>
|
|
220
|
+
<td className="px-6 py-4">
|
|
221
|
+
<div className="text-sm">{spec.title || spec.specName}</div>
|
|
222
|
+
</td>
|
|
223
|
+
<td className="px-6 py-4 whitespace-nowrap">
|
|
224
|
+
<StatusBadge status={spec.status || 'planned'} />
|
|
225
|
+
</td>
|
|
226
|
+
<td className="px-6 py-4 whitespace-nowrap">
|
|
227
|
+
<PriorityBadge priority={spec.priority || 'medium'} />
|
|
228
|
+
</td>
|
|
229
|
+
</tr>
|
|
230
|
+
))}
|
|
231
|
+
</tbody>
|
|
232
|
+
</table>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
{/* Mobile Card View */}
|
|
236
|
+
<div className="md:hidden space-y-3">
|
|
237
|
+
{filteredSpecs.map((spec) => (
|
|
238
|
+
<Link
|
|
239
|
+
key={spec.id}
|
|
240
|
+
href={`/specs/${spec.specNumber || spec.id}`}
|
|
241
|
+
className="block"
|
|
242
|
+
>
|
|
243
|
+
<Card className="hover:bg-muted/50 transition-colors">
|
|
244
|
+
<CardContent className="p-4">
|
|
245
|
+
<div className="flex items-start justify-between gap-2 mb-2">
|
|
246
|
+
<div className="flex-1 min-w-0">
|
|
247
|
+
<div className="font-medium text-primary mb-1">
|
|
248
|
+
{spec.specNumber ? `#${spec.specNumber}` : spec.specName}
|
|
249
|
+
</div>
|
|
250
|
+
<div className="text-sm text-foreground truncate">
|
|
251
|
+
{spec.title || spec.specName}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
<div className="flex items-center gap-2 mt-3">
|
|
256
|
+
<StatusBadge status={spec.status || 'planned'} />
|
|
257
|
+
<PriorityBadge priority={spec.priority || 'medium'} />
|
|
258
|
+
</div>
|
|
259
|
+
</CardContent>
|
|
260
|
+
</Card>
|
|
261
|
+
</Link>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
</section>
|
|
265
|
+
|
|
266
|
+
{/* Footer */}
|
|
267
|
+
<footer className="container mx-auto px-4 py-8 text-center text-muted-foreground border-t border-gray-200 dark:border-gray-800 mt-12">
|
|
268
|
+
<p>LeanSpec - Lightweight spec methodology for AI-powered development</p>
|
|
269
|
+
<p className="mt-2 text-sm">
|
|
270
|
+
<a href="https://github.com/codervisor/lean-spec" className="text-primary hover:underline">
|
|
271
|
+
View on GitHub
|
|
272
|
+
</a>
|
|
273
|
+
</p>
|
|
274
|
+
</footer>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import "./globals.css";
|
|
3
|
+
import { Navigation } from "@/components/navigation";
|
|
4
|
+
import { MainSidebar } from "@/components/main-sidebar";
|
|
5
|
+
import { ThemeProvider } from "@/components/theme-provider";
|
|
6
|
+
import { Toast } from "@/components/ui/toast";
|
|
7
|
+
import { getSpecs } from "@/lib/db/service-queries";
|
|
8
|
+
|
|
9
|
+
export const metadata: Metadata = {
|
|
10
|
+
title: "LeanSpec Web - Interactive Spec Showcase",
|
|
11
|
+
description: "Browse and explore LeanSpec specifications in a rich, interactive format",
|
|
12
|
+
icons: {
|
|
13
|
+
icon: [
|
|
14
|
+
{ url: '/favicon.ico' },
|
|
15
|
+
{ url: '/logo.svg', type: 'image/svg+xml' },
|
|
16
|
+
],
|
|
17
|
+
apple: '/logo.svg',
|
|
18
|
+
},
|
|
19
|
+
openGraph: {
|
|
20
|
+
title: "LeanSpec Web",
|
|
21
|
+
description: "Browse and explore LeanSpec specifications in a rich, interactive format",
|
|
22
|
+
type: "website",
|
|
23
|
+
},
|
|
24
|
+
viewport: {
|
|
25
|
+
width: 'device-width',
|
|
26
|
+
initialScale: 1,
|
|
27
|
+
maximumScale: 5,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default async function RootLayout({
|
|
32
|
+
children,
|
|
33
|
+
}: Readonly<{
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
}>) {
|
|
36
|
+
const specs = await getSpecs();
|
|
37
|
+
const specsForSearch = specs
|
|
38
|
+
.filter(spec => spec.specNumber && spec.title && spec.status && spec.priority)
|
|
39
|
+
.map(spec => ({
|
|
40
|
+
id: spec.id,
|
|
41
|
+
specNumber: spec.specNumber!.toString(),
|
|
42
|
+
title: spec.title!,
|
|
43
|
+
status: spec.status!,
|
|
44
|
+
priority: spec.priority!,
|
|
45
|
+
tags: spec.tags || [],
|
|
46
|
+
createdAt: spec.createdAt ? spec.createdAt.toISOString() : '',
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<html lang="en" suppressHydrationWarning>
|
|
51
|
+
<body className="antialiased bg-background overflow-x-hidden">
|
|
52
|
+
<ThemeProvider
|
|
53
|
+
attribute="class"
|
|
54
|
+
defaultTheme="system"
|
|
55
|
+
enableSystem
|
|
56
|
+
disableTransitionOnChange
|
|
57
|
+
>
|
|
58
|
+
<Navigation specs={specsForSearch} />
|
|
59
|
+
<div className="flex w-full min-w-0">
|
|
60
|
+
<MainSidebar />
|
|
61
|
+
<main className="flex-1 min-h-[calc(100vh-3.5rem)] min-w-0 w-full lg:w-[calc(100vw-var(--main-sidebar-width,240px))]">
|
|
62
|
+
{children}
|
|
63
|
+
</main>
|
|
64
|
+
</div>
|
|
65
|
+
<Toast />
|
|
66
|
+
</ThemeProvider>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
|
2
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
3
|
+
|
|
4
|
+
export default function Loading() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="min-h-screen bg-background">
|
|
7
|
+
{/* Stats Section */}
|
|
8
|
+
<section className="container mx-auto px-4 py-8">
|
|
9
|
+
<div className="mb-8">
|
|
10
|
+
<Skeleton className="h-10 w-64 mb-2" />
|
|
11
|
+
<Skeleton className="h-5 w-96" />
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
|
15
|
+
{[...Array(4)].map((_, i) => (
|
|
16
|
+
<Card key={i}>
|
|
17
|
+
<CardHeader className="pb-3">
|
|
18
|
+
<Skeleton className="h-4 w-24" />
|
|
19
|
+
</CardHeader>
|
|
20
|
+
<CardContent>
|
|
21
|
+
<Skeleton className="h-9 w-16" />
|
|
22
|
+
</CardContent>
|
|
23
|
+
</Card>
|
|
24
|
+
))}
|
|
25
|
+
</div>
|
|
26
|
+
</section>
|
|
27
|
+
|
|
28
|
+
{/* Specs List */}
|
|
29
|
+
<section className="container mx-auto px-4 py-8">
|
|
30
|
+
<div className="mb-6">
|
|
31
|
+
<Skeleton className="h-8 w-48 mb-4" />
|
|
32
|
+
|
|
33
|
+
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
|
34
|
+
<Skeleton className="h-9 flex-1" />
|
|
35
|
+
<div className="flex gap-2">
|
|
36
|
+
<Skeleton className="h-9 w-32" />
|
|
37
|
+
<Skeleton className="h-9 w-32" />
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<Skeleton className="h-4 w-48 mb-4" />
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<Card>
|
|
45
|
+
<div className="overflow-x-auto">
|
|
46
|
+
<table className="w-full">
|
|
47
|
+
<thead className="border-b">
|
|
48
|
+
<tr>
|
|
49
|
+
<th className="px-6 py-3 text-left">
|
|
50
|
+
<Skeleton className="h-4 w-12" />
|
|
51
|
+
</th>
|
|
52
|
+
<th className="px-6 py-3 text-left">
|
|
53
|
+
<Skeleton className="h-4 w-16" />
|
|
54
|
+
</th>
|
|
55
|
+
<th className="px-6 py-3 text-left">
|
|
56
|
+
<Skeleton className="h-4 w-16" />
|
|
57
|
+
</th>
|
|
58
|
+
<th className="px-6 py-3 text-left">
|
|
59
|
+
<Skeleton className="h-4 w-16" />
|
|
60
|
+
</th>
|
|
61
|
+
</tr>
|
|
62
|
+
</thead>
|
|
63
|
+
<tbody className="divide-y">
|
|
64
|
+
{[...Array(10)].map((_, i) => (
|
|
65
|
+
<tr key={i}>
|
|
66
|
+
<td className="px-6 py-4">
|
|
67
|
+
<Skeleton className="h-5 w-12" />
|
|
68
|
+
</td>
|
|
69
|
+
<td className="px-6 py-4">
|
|
70
|
+
<Skeleton className="h-5 w-64" />
|
|
71
|
+
</td>
|
|
72
|
+
<td className="px-6 py-4">
|
|
73
|
+
<Skeleton className="h-6 w-20" />
|
|
74
|
+
</td>
|
|
75
|
+
<td className="px-6 py-4">
|
|
76
|
+
<Skeleton className="h-6 w-16" />
|
|
77
|
+
</td>
|
|
78
|
+
</tr>
|
|
79
|
+
))}
|
|
80
|
+
</tbody>
|
|
81
|
+
</table>
|
|
82
|
+
</div>
|
|
83
|
+
</Card>
|
|
84
|
+
</section>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
|
|
4
|
+
export default function NotFound() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="min-h-screen bg-background flex items-center justify-center px-4">
|
|
7
|
+
<div className="text-center space-y-6">
|
|
8
|
+
<div className="space-y-2">
|
|
9
|
+
<h1 className="text-6xl font-bold text-primary">404</h1>
|
|
10
|
+
<h2 className="text-2xl font-semibold">Page Not Found</h2>
|
|
11
|
+
<p className="text-muted-foreground max-w-md mx-auto">
|
|
12
|
+
The page you’re looking for doesn’t exist or has been moved.
|
|
13
|
+
</p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div className="flex gap-4 justify-center">
|
|
17
|
+
<Button asChild>
|
|
18
|
+
<Link href="/">Go Home</Link>
|
|
19
|
+
</Button>
|
|
20
|
+
<Button variant="outline" asChild>
|
|
21
|
+
<Link href="/board">View Board</Link>
|
|
22
|
+
</Button>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home page - Dashboard with project overview
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getStats, getSpecs } from '@/lib/db/service-queries';
|
|
6
|
+
import { DashboardClient } from './dashboard-client';
|
|
7
|
+
|
|
8
|
+
// Force dynamic rendering - this page needs runtime data
|
|
9
|
+
export const dynamic = 'force-dynamic';
|
|
10
|
+
|
|
11
|
+
export default async function Home() {
|
|
12
|
+
const [stats, specs] = await Promise.all([
|
|
13
|
+
getStats(),
|
|
14
|
+
getSpecs(),
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
return <DashboardClient initialSpecs={specs} initialStats={stats} />;
|
|
18
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec detail page with markdown rendering and enhanced UI
|
|
3
|
+
* Phase 2: Tier 2 - Hybrid Rendering with client-side caching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { notFound } from 'next/navigation';
|
|
7
|
+
import { getSpecById, getSpecsWithSubSpecCount } from '@/lib/db/service-queries';
|
|
8
|
+
import { SpecDetailWrapper } from '@/components/spec-detail-wrapper';
|
|
9
|
+
|
|
10
|
+
// Tier 1: Route segment caching for performance
|
|
11
|
+
export const revalidate = 60; // Cache rendered pages for 60s
|
|
12
|
+
export const dynamicParams = true; // Generate new pages on demand
|
|
13
|
+
|
|
14
|
+
export default async function SpecDetailPage({
|
|
15
|
+
params,
|
|
16
|
+
searchParams
|
|
17
|
+
}: {
|
|
18
|
+
params: Promise<{ id: string }>;
|
|
19
|
+
searchParams: Promise<{ subspec?: string }>;
|
|
20
|
+
}) {
|
|
21
|
+
const { id } = await params;
|
|
22
|
+
const { subspec: currentSubSpec } = await searchParams;
|
|
23
|
+
|
|
24
|
+
const [spec, allSpecs] = await Promise.all([
|
|
25
|
+
getSpecById(id),
|
|
26
|
+
getSpecsWithSubSpecCount()
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
if (!spec) {
|
|
30
|
+
notFound();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<SpecDetailWrapper
|
|
35
|
+
spec={spec}
|
|
36
|
+
allSpecs={allSpecs}
|
|
37
|
+
currentSubSpec={currentSubSpec}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specs page - Browse all LeanSpec specifications with list/board switcher
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getSpecs, getStats } from '@/lib/db/service-queries';
|
|
6
|
+
import { SpecsClient } from './specs-client';
|
|
7
|
+
|
|
8
|
+
// Force dynamic rendering - this page needs runtime data
|
|
9
|
+
export const dynamic = 'force-dynamic';
|
|
10
|
+
|
|
11
|
+
export default async function SpecsPage() {
|
|
12
|
+
const [specs, stats] = await Promise.all([
|
|
13
|
+
getSpecs(),
|
|
14
|
+
getStats(),
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
return <SpecsClient initialSpecs={specs} initialStats={stats} />;
|
|
18
|
+
}
|