@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.
Files changed (187) hide show
  1. package/dist/standalone/packages/web/.next/BUILD_ID +1 -1
  2. package/dist/standalone/packages/web/.next/build-manifest.json +2 -2
  3. package/dist/standalone/packages/web/.next/prerender-manifest.json +3 -3
  4. package/dist/standalone/packages/web/.next/server/app/_global-error.html +2 -2
  5. package/dist/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
  6. package/dist/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  9. package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/dist/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/dist/standalone/packages/web/.next/server/app/_not-found.html +2 -2
  12. package/dist/standalone/packages/web/.next/server/app/_not-found.rsc +2 -2
  13. package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  14. package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  15. package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  16. package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  17. package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  18. package/dist/standalone/packages/web/.next/server/app/api/projects/[id]/specs/route.js.nft.json +1 -1
  19. package/dist/standalone/packages/web/.next/server/app/api/projects/route.js.nft.json +1 -1
  20. package/dist/standalone/packages/web/.next/server/app/api/revalidate/route.js.nft.json +1 -1
  21. package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/dependency-graph/route.js.nft.json +1 -1
  22. package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/route.js.nft.json +1 -1
  23. package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/subspecs/[file]/route.js.nft.json +1 -1
  24. package/dist/standalone/packages/web/.next/server/app/api/stats/route.js.nft.json +1 -1
  25. package/dist/standalone/packages/web/.next/server/app/board/page.js.nft.json +1 -1
  26. package/dist/standalone/packages/web/.next/server/app/board.html +1 -1
  27. package/dist/standalone/packages/web/.next/server/app/board.rsc +2 -2
  28. package/dist/standalone/packages/web/.next/server/app/board.segments/_full.segment.rsc +2 -2
  29. package/dist/standalone/packages/web/.next/server/app/board.segments/_index.segment.rsc +1 -1
  30. package/dist/standalone/packages/web/.next/server/app/board.segments/_tree.segment.rsc +1 -1
  31. package/dist/standalone/packages/web/.next/server/app/board.segments/board/__PAGE__.segment.rsc +1 -1
  32. package/dist/standalone/packages/web/.next/server/app/board.segments/board.segment.rsc +1 -1
  33. package/dist/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
  34. package/dist/standalone/packages/web/.next/server/app/specs/[id]/page.js.nft.json +1 -1
  35. package/dist/standalone/packages/web/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
  36. package/dist/standalone/packages/web/.next/server/app/specs/page.js.nft.json +1 -1
  37. package/dist/standalone/packages/web/.next/server/app/stats/page.js.nft.json +1 -1
  38. package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2e0f9179._.js +1 -1
  39. package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__577d6d08._.js +1 -1
  40. package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e54bc4b8._.js +1 -1
  41. package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f8978f3e._.js +1 -1
  42. package/dist/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__be46bb7c._.js +1 -1
  43. package/dist/standalone/packages/web/.next/server/chunks/ssr/_7dedc302._.js +1 -1
  44. package/dist/standalone/packages/web/.next/server/chunks/ssr/_ad71cd8c._.js +1 -1
  45. package/dist/standalone/packages/web/.next/server/chunks/ssr/_c5a5c652._.js +1 -1
  46. package/dist/standalone/packages/web/.next/server/pages/404.html +2 -2
  47. package/dist/standalone/packages/web/.next/server/pages/500.html +2 -2
  48. package/dist/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
  49. package/dist/standalone/packages/web/.next/server/server-reference-manifest.json +1 -1
  50. package/dist/{static/chunks/0de258404bcae76f.js → standalone/packages/web/.next/static/chunks/8864b47e107cbe63.js} +1 -1
  51. package/dist/{static/chunks/09ff02250dd56621.js → standalone/packages/web/.next/static/chunks/a2889ecda42c83e7.js} +1 -1
  52. package/dist/standalone/packages/web/.next/static/chunks/c22619397bb8368e.js +1 -0
  53. package/dist/standalone/packages/web/components.json +20 -0
  54. package/dist/standalone/packages/web/drizzle/0000_reflective_thena.sql +59 -0
  55. package/dist/standalone/packages/web/drizzle/0001_fresh_carmella_unuscione.sql +1 -0
  56. package/dist/standalone/packages/web/drizzle/meta/0000_snapshot.json +427 -0
  57. package/dist/standalone/packages/web/drizzle/meta/0001_snapshot.json +436 -0
  58. package/dist/standalone/packages/web/drizzle/meta/_journal.json +20 -0
  59. package/dist/standalone/packages/web/drizzle.config.ts +10 -0
  60. package/dist/standalone/packages/web/eslint.config.mjs +18 -0
  61. package/dist/standalone/packages/web/next.config.ts +7 -0
  62. package/dist/standalone/packages/web/package.json +1 -1
  63. package/dist/standalone/packages/web/postcss.config.mjs +8 -0
  64. package/dist/standalone/packages/web/src/app/api/projects/[id]/specs/route.ts +23 -0
  65. package/dist/standalone/packages/web/src/app/api/projects/route.ts +19 -0
  66. package/dist/standalone/packages/web/src/app/api/revalidate/route.ts +63 -0
  67. package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.test.ts +51 -0
  68. package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.ts +171 -0
  69. package/dist/standalone/packages/web/src/app/api/specs/[id]/route.ts +36 -0
  70. package/dist/standalone/packages/web/src/app/api/specs/[id]/subspecs/[file]/route.ts +46 -0
  71. package/dist/standalone/packages/web/src/app/api/stats/route.ts +19 -0
  72. package/dist/standalone/packages/web/src/app/board/board-client.tsx +162 -0
  73. package/dist/standalone/packages/web/src/app/board/loading.tsx +43 -0
  74. package/dist/standalone/packages/web/src/app/board/page.tsx +18 -0
  75. package/dist/standalone/packages/web/src/app/dashboard-client.tsx +364 -0
  76. package/dist/standalone/packages/web/src/app/error.tsx +43 -0
  77. package/dist/standalone/packages/web/src/app/globals.css +531 -0
  78. package/dist/standalone/packages/web/src/app/home-client.tsx +277 -0
  79. package/dist/standalone/packages/web/src/app/layout.tsx +70 -0
  80. package/dist/standalone/packages/web/src/app/loading.tsx +87 -0
  81. package/dist/standalone/packages/web/src/app/not-found.tsx +27 -0
  82. package/dist/standalone/packages/web/src/app/page.tsx +18 -0
  83. package/dist/standalone/packages/web/src/app/specs/[id]/loading.tsx +5 -0
  84. package/dist/standalone/packages/web/src/app/specs/[id]/page.tsx +43 -0
  85. package/dist/standalone/packages/web/src/app/specs/page.tsx +18 -0
  86. package/dist/standalone/packages/web/src/app/specs/specs-client.tsx +425 -0
  87. package/dist/standalone/packages/web/src/app/stats/page.tsx +18 -0
  88. package/dist/standalone/packages/web/src/app/stats/stats-client.tsx +283 -0
  89. package/dist/standalone/packages/web/src/components/back-to-top.tsx +46 -0
  90. package/dist/standalone/packages/web/src/components/empty-state.tsx +52 -0
  91. package/dist/standalone/packages/web/src/components/main-sidebar.tsx +175 -0
  92. package/dist/standalone/packages/web/src/components/markdown-link.test.ts +96 -0
  93. package/dist/standalone/packages/web/src/components/markdown-link.tsx +95 -0
  94. package/dist/standalone/packages/web/src/components/navigation.tsx +210 -0
  95. package/dist/standalone/packages/web/src/components/priority-badge.tsx +53 -0
  96. package/dist/standalone/packages/web/src/components/quick-search.tsx +180 -0
  97. package/dist/standalone/packages/web/src/components/skeletons.tsx +119 -0
  98. package/dist/standalone/packages/web/src/components/spec-dependency-graph.tsx +369 -0
  99. package/dist/standalone/packages/web/src/components/spec-detail-client.tsx +372 -0
  100. package/dist/standalone/packages/web/src/components/spec-detail-loading-shell.tsx +42 -0
  101. package/dist/standalone/packages/web/src/components/spec-detail-wrapper.tsx +70 -0
  102. package/dist/standalone/packages/web/src/components/spec-metadata.tsx +136 -0
  103. package/dist/standalone/packages/web/src/components/spec-sidebar.tsx +127 -0
  104. package/dist/standalone/packages/web/src/components/spec-timeline.tsx +186 -0
  105. package/dist/standalone/packages/web/src/components/specs-nav-sidebar.tsx +561 -0
  106. package/dist/standalone/packages/web/src/components/status-badge.tsx +53 -0
  107. package/dist/standalone/packages/web/src/components/sub-spec-tabs.tsx +143 -0
  108. package/dist/standalone/packages/web/src/components/table-of-contents.tsx +130 -0
  109. package/dist/standalone/packages/web/src/components/theme-provider.tsx +11 -0
  110. package/dist/standalone/packages/web/src/components/theme-toggle.tsx +37 -0
  111. package/dist/standalone/packages/web/src/components/ui/avatar.tsx +50 -0
  112. package/dist/standalone/packages/web/src/components/ui/badge.tsx +36 -0
  113. package/dist/standalone/packages/web/src/components/ui/breadcrumb.tsx +110 -0
  114. package/dist/standalone/packages/web/src/components/ui/button.tsx +57 -0
  115. package/dist/standalone/packages/web/src/components/ui/card.tsx +76 -0
  116. package/dist/standalone/packages/web/src/components/ui/command.tsx +153 -0
  117. package/dist/standalone/packages/web/src/components/ui/dialog.tsx +122 -0
  118. package/dist/standalone/packages/web/src/components/ui/input.tsx +24 -0
  119. package/dist/standalone/packages/web/src/components/ui/select.tsx +159 -0
  120. package/dist/standalone/packages/web/src/components/ui/separator.tsx +31 -0
  121. package/dist/standalone/packages/web/src/components/ui/skeleton.tsx +15 -0
  122. package/dist/standalone/packages/web/src/components/ui/tabs.tsx +55 -0
  123. package/dist/standalone/packages/web/src/components/ui/toast.tsx +30 -0
  124. package/dist/standalone/packages/web/src/components/ui/tooltip.tsx +32 -0
  125. package/dist/standalone/packages/web/src/lib/date-utils.ts +76 -0
  126. package/dist/standalone/packages/web/src/lib/db/index.ts +42 -0
  127. package/dist/standalone/packages/web/src/lib/db/migrate.ts +18 -0
  128. package/dist/standalone/packages/web/src/lib/db/queries.ts +114 -0
  129. package/dist/standalone/packages/web/src/lib/db/schema.ts +123 -0
  130. package/dist/standalone/packages/web/src/lib/db/seed.ts +156 -0
  131. package/dist/standalone/packages/web/src/lib/db/service-queries.ts +216 -0
  132. package/dist/standalone/packages/web/src/lib/dependency-graph.ts +105 -0
  133. package/dist/standalone/packages/web/src/lib/specs/service.ts +120 -0
  134. package/dist/standalone/packages/web/src/lib/specs/sources/database-source.ts +94 -0
  135. package/dist/standalone/packages/web/src/lib/specs/sources/filesystem-source.ts +249 -0
  136. package/dist/standalone/packages/web/src/lib/specs/types.ts +55 -0
  137. package/dist/standalone/packages/web/src/lib/stores/specs-sidebar-store.ts +152 -0
  138. package/dist/standalone/packages/web/src/lib/sub-specs.ts +171 -0
  139. package/dist/standalone/packages/web/src/lib/utils.ts +17 -0
  140. package/dist/standalone/packages/web/src/types/specs.ts +18 -0
  141. package/dist/standalone/packages/web/tailwind.config.ts +58 -0
  142. package/dist/standalone/packages/web/tsconfig.json +34 -0
  143. package/dist/standalone/packages/web/vitest.config.ts +14 -0
  144. package/dist/standalone/specs/100-release-process-typecheck-failure/README.md +266 -0
  145. package/dist/standalone/specs/101-sidebar-scroll-position-drift/README.md +100 -0
  146. package/package.json +5 -3
  147. package/dist/BUILD_ID +0 -1
  148. package/dist/static/chunks/a3e649fcddd3d715.js +0 -1
  149. /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_buildManifest.js +0 -0
  150. /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_clientMiddlewareManifest.json +0 -0
  151. /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_ssgManifest.js +0 -0
  152. /package/dist/{static → standalone/packages/web/.next/static}/chunks/0c19c69aa7625475.js +0 -0
  153. /package/dist/{static → standalone/packages/web/.next/static}/chunks/116800b03245a1e5.js +0 -0
  154. /package/dist/{static → standalone/packages/web/.next/static}/chunks/19e80edf527aef5c.js +0 -0
  155. /package/dist/{static → standalone/packages/web/.next/static}/chunks/2ece90370908f56c.js +0 -0
  156. /package/dist/{static → standalone/packages/web/.next/static}/chunks/36fd2dddb486f6bc.js +0 -0
  157. /package/dist/{static → standalone/packages/web/.next/static}/chunks/5c2072ad938de8ed.js +0 -0
  158. /package/dist/{static → standalone/packages/web/.next/static}/chunks/6577fe797a336bab.js +0 -0
  159. /package/dist/{static → standalone/packages/web/.next/static}/chunks/6a05a93ec8fa7b83.js +0 -0
  160. /package/dist/{static → standalone/packages/web/.next/static}/chunks/7f732ea69e643219.js +0 -0
  161. /package/dist/{static → standalone/packages/web/.next/static}/chunks/a02c1f50ff00204f.js +0 -0
  162. /package/dist/{static → standalone/packages/web/.next/static}/chunks/a45464b9776dd88e.js +0 -0
  163. /package/dist/{static → standalone/packages/web/.next/static}/chunks/a6dad97d9634a72d.js +0 -0
  164. /package/dist/{static → standalone/packages/web/.next/static}/chunks/ae04dcd433be6dab.js +0 -0
  165. /package/dist/{static → standalone/packages/web/.next/static}/chunks/b20313408e970968.css +0 -0
  166. /package/dist/{static → standalone/packages/web/.next/static}/chunks/c46095e1a421d93f.js +0 -0
  167. /package/dist/{static → standalone/packages/web/.next/static}/chunks/c48dd4c72d7c5ef4.js +0 -0
  168. /package/dist/{static → standalone/packages/web/.next/static}/chunks/c557ac675be79771.js +0 -0
  169. /package/dist/{static → standalone/packages/web/.next/static}/chunks/dca0c854c59234cd.js +0 -0
  170. /package/dist/{static → standalone/packages/web/.next/static}/chunks/df1731c03abf1aee.css +0 -0
  171. /package/dist/{static → standalone/packages/web/.next/static}/chunks/dfd41488ad062cd5.js +0 -0
  172. /package/dist/{static → standalone/packages/web/.next/static}/chunks/ebd89051637b9a47.js +0 -0
  173. /package/dist/{static → standalone/packages/web/.next/static}/chunks/f3ec9fd77a8618b1.js +0 -0
  174. /package/dist/{static → standalone/packages/web/.next/static}/chunks/turbopack-7450632b40b2e378.js +0 -0
  175. /package/dist/{public → standalone/packages/web/public}/f864aa7e7061c0600e35cf3d879b27cf.txt +0 -0
  176. /package/dist/{public → standalone/packages/web/public}/favicon.ico +0 -0
  177. /package/dist/{public → standalone/packages/web/public}/file.svg +0 -0
  178. /package/dist/{public → standalone/packages/web/public}/github-mark-white.svg +0 -0
  179. /package/dist/{public → standalone/packages/web/public}/github-mark.svg +0 -0
  180. /package/dist/{public → standalone/packages/web/public}/globe.svg +0 -0
  181. /package/dist/{public → standalone/packages/web/public}/icon.svg +0 -0
  182. /package/dist/{public → standalone/packages/web/public}/logo-dark-bg.svg +0 -0
  183. /package/dist/{public → standalone/packages/web/public}/logo-with-bg.svg +0 -0
  184. /package/dist/{public → standalone/packages/web/public}/logo.svg +0 -0
  185. /package/dist/{public → standalone/packages/web/public}/next.svg +0 -0
  186. /package/dist/{public → standalone/packages/web/public}/vercel.svg +0 -0
  187. /package/dist/{public → standalone/packages/web/public}/window.svg +0 -0
@@ -0,0 +1,425 @@
1
+ 'use client';
2
+
3
+ import { useState, useMemo, useEffect, useRef } from 'react';
4
+ import Link from 'next/link';
5
+ import { useSearchParams, useRouter } from 'next/navigation';
6
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
7
+ import { Badge } from '@/components/ui/badge';
8
+ import { Input } from '@/components/ui/input';
9
+ import { Button } from '@/components/ui/button';
10
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue
16
+ } from '@/components/ui/select';
17
+ import {
18
+ Search,
19
+ CheckCircle2,
20
+ PlayCircle,
21
+ Clock,
22
+ Archive,
23
+ LayoutGrid,
24
+ List as ListIcon
25
+ } from 'lucide-react';
26
+ import { StatusBadge } from '@/components/status-badge';
27
+ import { PriorityBadge } from '@/components/priority-badge';
28
+ import { cn } from '@/lib/utils';
29
+
30
+ interface Spec {
31
+ id: string;
32
+ specNumber: number | null;
33
+ specName: string;
34
+ title: string | null;
35
+ status: string | null;
36
+ priority: string | null;
37
+ tags: string[] | null;
38
+ updatedAt: Date | null;
39
+ }
40
+
41
+ interface Stats {
42
+ totalSpecs: number;
43
+ completionRate: number;
44
+ specsByStatus: { status: string; count: number }[];
45
+ }
46
+
47
+ interface SpecsClientProps {
48
+ initialSpecs: Spec[];
49
+ initialStats: Stats;
50
+ }
51
+
52
+ type ViewMode = 'list' | 'board';
53
+ type SortBy = 'id-desc' | 'id-asc' | 'updated-desc' | 'title-asc';
54
+
55
+ export function SpecsClient({ initialSpecs }: SpecsClientProps) {
56
+ const searchParams = useSearchParams();
57
+ const router = useRouter();
58
+
59
+ const [searchQuery, setSearchQuery] = useState('');
60
+ const [statusFilter, setStatusFilter] = useState<string>('all');
61
+ const [priorityFilter, setPriorityFilter] = useState<string>('all');
62
+ const [sortBy, setSortBy] = useState<SortBy>('id-desc');
63
+ const [viewMode, setViewMode] = useState<ViewMode>(() => {
64
+ // Initialize from URL or localStorage
65
+ const urlView = searchParams.get('view');
66
+ if (urlView === 'board' || urlView === 'list') return urlView;
67
+
68
+ if (typeof window !== 'undefined') {
69
+ const stored = localStorage.getItem('specs-view-mode');
70
+ if (stored === 'board' || stored === 'list') return stored;
71
+ }
72
+ return 'list';
73
+ });
74
+
75
+ const isFirstRender = useRef(true);
76
+
77
+ // Update URL when view mode changes (skip on initial mount)
78
+ useEffect(() => {
79
+ if (isFirstRender.current) {
80
+ isFirstRender.current = false;
81
+ return;
82
+ }
83
+
84
+ const current = new URLSearchParams(window.location.search);
85
+ if (viewMode === 'board') {
86
+ current.set('view', 'board');
87
+ } else {
88
+ current.delete('view');
89
+ }
90
+ const search = current.toString();
91
+ const query = search ? `?${search}` : '';
92
+ router.replace(`/specs${query}`, { scroll: false });
93
+
94
+ // Persist to localStorage
95
+ if (typeof window !== 'undefined') {
96
+ localStorage.setItem('specs-view-mode', viewMode);
97
+ }
98
+ }, [viewMode, router]);
99
+
100
+ const filteredAndSortedSpecs = useMemo(() => {
101
+ const specs = initialSpecs.filter(spec => {
102
+ const matchesSearch = !searchQuery ||
103
+ spec.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
104
+ spec.specName.toLowerCase().includes(searchQuery.toLowerCase()) ||
105
+ spec.tags?.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
106
+
107
+ const matchesStatus = statusFilter === 'all' || spec.status === statusFilter;
108
+ const matchesPriority = priorityFilter === 'all' || spec.priority === priorityFilter;
109
+
110
+ return matchesSearch && matchesStatus && matchesPriority;
111
+ });
112
+
113
+ // Sort
114
+ switch (sortBy) {
115
+ case 'id-desc':
116
+ specs.sort((a, b) => (b.specNumber || 0) - (a.specNumber || 0));
117
+ break;
118
+ case 'id-asc':
119
+ specs.sort((a, b) => (a.specNumber || 0) - (b.specNumber || 0));
120
+ break;
121
+ case 'updated-desc':
122
+ specs.sort((a, b) => {
123
+ if (!a.updatedAt) return 1;
124
+ if (!b.updatedAt) return -1;
125
+ return b.updatedAt.getTime() - a.updatedAt.getTime();
126
+ });
127
+ break;
128
+ case 'title-asc':
129
+ specs.sort((a, b) => {
130
+ const titleA = (a.title || a.specName).toLowerCase();
131
+ const titleB = (b.title || b.specName).toLowerCase();
132
+ return titleA.localeCompare(titleB);
133
+ });
134
+ break;
135
+ }
136
+
137
+ return specs;
138
+ }, [initialSpecs, searchQuery, statusFilter, priorityFilter, sortBy]);
139
+
140
+ return (
141
+ <div className="min-h-screen bg-background p-8">
142
+ <div className="max-w-7xl mx-auto">
143
+ <div className="mb-8">
144
+ <h1 className="text-4xl font-bold tracking-tight">Specifications</h1>
145
+ <p className="text-muted-foreground mt-2">
146
+ {viewMode === 'board' ? 'Kanban board view' : 'Browse all specifications'}
147
+ </p>
148
+ </div>
149
+
150
+ {/* Filters and View Switcher */}
151
+ <div className="flex flex-col gap-4 mb-6">
152
+ <div className="flex flex-col sm:flex-row gap-4">
153
+ {/* Search */}
154
+ <div className="flex-1 relative">
155
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
156
+ <Input
157
+ placeholder="Search specs..."
158
+ value={searchQuery}
159
+ onChange={(e) => setSearchQuery(e.target.value)}
160
+ className="pl-10"
161
+ />
162
+ </div>
163
+
164
+ {/* Status Filter */}
165
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
166
+ <SelectTrigger className="w-full sm:w-[180px]">
167
+ <SelectValue placeholder="Status" />
168
+ </SelectTrigger>
169
+ <SelectContent>
170
+ <SelectItem value="all">All Status</SelectItem>
171
+ <SelectItem value="planned">Planned</SelectItem>
172
+ <SelectItem value="in-progress">In Progress</SelectItem>
173
+ <SelectItem value="complete">Complete</SelectItem>
174
+ <SelectItem value="archived">Archived</SelectItem>
175
+ </SelectContent>
176
+ </Select>
177
+
178
+ {/* Priority Filter */}
179
+ <Select value={priorityFilter} onValueChange={setPriorityFilter}>
180
+ <SelectTrigger className="w-full sm:w-[180px]">
181
+ <SelectValue placeholder="Priority" />
182
+ </SelectTrigger>
183
+ <SelectContent>
184
+ <SelectItem value="all">All Priority</SelectItem>
185
+ <SelectItem value="critical">Critical</SelectItem>
186
+ <SelectItem value="high">High</SelectItem>
187
+ <SelectItem value="medium">Medium</SelectItem>
188
+ <SelectItem value="low">Low</SelectItem>
189
+ </SelectContent>
190
+ </Select>
191
+ </div>
192
+
193
+ <div className="flex flex-col sm:flex-row justify-between gap-4">
194
+ {/* Sort Controls */}
195
+ <Select value={sortBy} onValueChange={(v) => setSortBy(v as SortBy)}>
196
+ <SelectTrigger className="w-full sm:w-[220px]">
197
+ <SelectValue placeholder="Sort by" />
198
+ </SelectTrigger>
199
+ <SelectContent>
200
+ <SelectItem value="id-desc">Newest First (ID ↓)</SelectItem>
201
+ <SelectItem value="id-asc">Oldest First (ID ↑)</SelectItem>
202
+ <SelectItem value="updated-desc">Recently Updated</SelectItem>
203
+ <SelectItem value="title-asc">Title (A-Z)</SelectItem>
204
+ </SelectContent>
205
+ </Select>
206
+
207
+ {/* View Mode Switcher */}
208
+ <div className="flex gap-2">
209
+ <Button
210
+ variant={viewMode === 'list' ? 'default' : 'outline'}
211
+ size="sm"
212
+ onClick={() => setViewMode('list')}
213
+ className="flex items-center gap-2"
214
+ >
215
+ <ListIcon className="h-4 w-4" />
216
+ List
217
+ </Button>
218
+ <Button
219
+ variant={viewMode === 'board' ? 'default' : 'outline'}
220
+ size="sm"
221
+ onClick={() => setViewMode('board')}
222
+ className="flex items-center gap-2"
223
+ >
224
+ <LayoutGrid className="h-4 w-4" />
225
+ Board
226
+ </Button>
227
+ </div>
228
+ </div>
229
+ </div>
230
+
231
+ {/* Results count */}
232
+ <div className="text-sm text-muted-foreground mb-4">
233
+ Showing {filteredAndSortedSpecs.length} of {initialSpecs.length} specs
234
+ </div>
235
+
236
+ {/* Content based on view mode */}
237
+ {viewMode === 'list' ? (
238
+ <ListView specs={filteredAndSortedSpecs} />
239
+ ) : (
240
+ <BoardView specs={filteredAndSortedSpecs} />
241
+ )}
242
+ </div>
243
+ </div>
244
+ );
245
+ }
246
+
247
+ function ListView({ specs }: { specs: Spec[] }) {
248
+ return (
249
+ <div className="grid grid-cols-1 gap-4">
250
+ {specs.map(spec => {
251
+ const priorityColors = {
252
+ 'critical': 'border-l-red-500',
253
+ 'high': 'border-l-orange-500',
254
+ 'medium': 'border-l-blue-500',
255
+ 'low': 'border-l-gray-400'
256
+ };
257
+ const borderColor = priorityColors[spec.priority as keyof typeof priorityColors] || 'border-l-gray-300';
258
+
259
+ return (
260
+ <Card
261
+ key={spec.id}
262
+ className={cn(
263
+ "hover:shadow-lg transition-all duration-150 hover:scale-[1.01] border-l-4 cursor-pointer",
264
+ borderColor
265
+ )}
266
+ onClick={() => window.location.href = `/specs/${spec.specNumber || spec.id}`}
267
+ >
268
+ <CardHeader className="pb-3">
269
+ <div className="flex items-start justify-between gap-4">
270
+ <div className="flex-1 min-w-0">
271
+ <Link href={`/specs/${spec.specNumber || spec.id}`}>
272
+ <CardTitle className="text-lg font-semibold hover:text-primary transition-colors">
273
+ {spec.specNumber ? `#${spec.specNumber.toString().padStart(3, '0')}` : spec.specName}
274
+ {' '}
275
+ {spec.title || spec.specName}
276
+ </CardTitle>
277
+ </Link>
278
+ </div>
279
+ <div className="flex gap-2 shrink-0">
280
+ {spec.status && <StatusBadge status={spec.status} />}
281
+ {spec.priority && <PriorityBadge priority={spec.priority} />}
282
+ </div>
283
+ </div>
284
+ </CardHeader>
285
+ {spec.tags && spec.tags.length > 0 && (
286
+ <CardContent>
287
+ <div className="flex flex-wrap gap-2">
288
+ {spec.tags.map(tag => (
289
+ <Badge key={tag} variant="secondary">
290
+ {tag}
291
+ </Badge>
292
+ ))}
293
+ </div>
294
+ </CardContent>
295
+ )}
296
+ </Card>
297
+ );
298
+ })}
299
+ </div>
300
+ );
301
+ }
302
+
303
+ function BoardView({ specs }: { specs: Spec[] }) {
304
+ const columns = useMemo(() => {
305
+ const statusConfig = {
306
+ 'planned': {
307
+ icon: Clock,
308
+ title: 'Planned',
309
+ colorClass: 'text-blue-600 dark:text-blue-400',
310
+ bgClass: 'bg-blue-50 dark:bg-blue-900/20',
311
+ borderClass: 'border-blue-200 dark:border-blue-800'
312
+ },
313
+ 'in-progress': {
314
+ icon: PlayCircle,
315
+ title: 'In Progress',
316
+ colorClass: 'text-orange-600 dark:text-orange-400',
317
+ bgClass: 'bg-orange-50 dark:bg-orange-900/20',
318
+ borderClass: 'border-orange-200 dark:border-orange-800'
319
+ },
320
+ 'complete': {
321
+ icon: CheckCircle2,
322
+ title: 'Complete',
323
+ colorClass: 'text-green-600 dark:text-green-400',
324
+ bgClass: 'bg-green-50 dark:bg-green-900/20',
325
+ borderClass: 'border-green-200 dark:border-green-800'
326
+ },
327
+ 'archived': {
328
+ icon: Archive,
329
+ title: 'Archived',
330
+ colorClass: 'text-gray-600 dark:text-gray-400',
331
+ bgClass: 'bg-gray-50 dark:bg-gray-900/20',
332
+ borderClass: 'border-gray-200 dark:border-gray-800'
333
+ }
334
+ };
335
+
336
+ const statuses = ['planned', 'in-progress', 'complete', 'archived'] as const;
337
+
338
+ return statuses.map(status => ({
339
+ status,
340
+ config: statusConfig[status],
341
+ specs: specs.filter(spec => spec.status === status),
342
+ }));
343
+ }, [specs]);
344
+
345
+ return (
346
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
347
+ {columns.map(column => {
348
+ const Icon = column.config.icon;
349
+ return (
350
+ <div key={column.status} className="flex flex-col">
351
+ <div className={cn(
352
+ "sticky top-14 z-40 mb-4 p-3 rounded-lg border-2 bg-background",
353
+ column.config.bgClass,
354
+ column.config.borderClass
355
+ )}>
356
+ <h2 className={cn(
357
+ "text-lg font-semibold flex items-center gap-2",
358
+ column.config.colorClass
359
+ )}>
360
+ <Icon className="h-5 w-5" />
361
+ {column.config.title}
362
+ <Badge variant="outline" className="ml-auto">{column.specs.length}</Badge>
363
+ </h2>
364
+ </div>
365
+
366
+ <div className="space-y-3 flex-1">
367
+ {column.specs.map(spec => {
368
+ const priorityColors = {
369
+ 'critical': 'border-l-red-500',
370
+ 'high': 'border-l-orange-500',
371
+ 'medium': 'border-l-blue-500',
372
+ 'low': 'border-l-gray-400'
373
+ };
374
+ const borderColor = priorityColors[spec.priority as keyof typeof priorityColors] || 'border-l-gray-300';
375
+
376
+ return (
377
+ <Card
378
+ key={spec.id}
379
+ className={cn(
380
+ "hover:shadow-lg transition-all duration-150 hover:scale-[1.02] border-l-4 cursor-pointer",
381
+ borderColor
382
+ )}
383
+ onClick={() => window.location.href = `/specs/${spec.specNumber || spec.id}`}
384
+ >
385
+ <CardHeader className="pb-3">
386
+ <Link href={`/specs/${spec.specNumber || spec.id}`}>
387
+ <CardTitle className="text-sm font-medium hover:text-primary transition-colors">
388
+ {spec.specNumber ? `#${spec.specNumber}` : spec.specName}
389
+ </CardTitle>
390
+ </Link>
391
+ </CardHeader>
392
+ <CardContent className="space-y-2">
393
+ <p className="text-sm text-muted-foreground line-clamp-2">
394
+ {spec.title || spec.specName}
395
+ </p>
396
+
397
+ <div className="flex items-center gap-2 flex-wrap">
398
+ {spec.priority && <PriorityBadge priority={spec.priority} />}
399
+
400
+ {spec.tags && spec.tags.length > 0 && (
401
+ <>
402
+ {spec.tags.slice(0, 2).map(tag => (
403
+ <Badge key={tag} variant="outline" className="text-xs">
404
+ {tag}
405
+ </Badge>
406
+ ))}
407
+ {spec.tags.length > 2 && (
408
+ <Badge variant="outline" className="text-xs">
409
+ +{spec.tags.length - 2}
410
+ </Badge>
411
+ )}
412
+ </>
413
+ )}
414
+ </div>
415
+ </CardContent>
416
+ </Card>
417
+ );
418
+ })}
419
+ </div>
420
+ </div>
421
+ );
422
+ })}
423
+ </div>
424
+ );
425
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Stats page with detailed metrics and charts
3
+ */
4
+
5
+ import { getStats, getSpecs } from '@/lib/db/service-queries';
6
+ import { StatsClient } from './stats-client';
7
+
8
+ // Force dynamic rendering
9
+ export const dynamic = 'force-dynamic';
10
+
11
+ export default async function StatsPage() {
12
+ const [stats, specs] = await Promise.all([
13
+ getStats(),
14
+ getSpecs(),
15
+ ]);
16
+
17
+ return <StatsClient stats={stats} specs={specs} />;
18
+ }