@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,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client component for spec detail page with SWR caching and instant sub-spec navigation
|
|
3
|
+
* Phase 2: Tier 2 - Hybrid Rendering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
10
|
+
import useSWR from 'swr';
|
|
11
|
+
import ReactMarkdown from 'react-markdown';
|
|
12
|
+
import remarkGfm from 'remark-gfm';
|
|
13
|
+
import rehypeHighlight from 'rehype-highlight';
|
|
14
|
+
import rehypeSlug from 'rehype-slug';
|
|
15
|
+
import { Badge } from '@/components/ui/badge';
|
|
16
|
+
import { Button } from '@/components/ui/button';
|
|
17
|
+
import { SpecTimeline } from '@/components/spec-timeline';
|
|
18
|
+
import { StatusBadge } from '@/components/status-badge';
|
|
19
|
+
import { PriorityBadge } from '@/components/priority-badge';
|
|
20
|
+
import { MarkdownLink } from '@/components/markdown-link';
|
|
21
|
+
import { TableOfContents } from '@/components/table-of-contents';
|
|
22
|
+
import { BackToTop } from '@/components/back-to-top';
|
|
23
|
+
import { SpecDependencyGraph } from '@/components/spec-dependency-graph';
|
|
24
|
+
import {
|
|
25
|
+
Dialog,
|
|
26
|
+
DialogContent,
|
|
27
|
+
DialogDescription,
|
|
28
|
+
DialogHeader,
|
|
29
|
+
DialogTitle,
|
|
30
|
+
DialogTrigger,
|
|
31
|
+
} from '@/components/ui/dialog';
|
|
32
|
+
import { extractH1Title, cn } from '@/lib/utils';
|
|
33
|
+
import { formatRelativeTime } from '@/lib/date-utils';
|
|
34
|
+
import type { SpecWithMetadata } from '@/types/specs';
|
|
35
|
+
import {
|
|
36
|
+
FileText,
|
|
37
|
+
Palette,
|
|
38
|
+
Code,
|
|
39
|
+
TestTube,
|
|
40
|
+
CheckSquare,
|
|
41
|
+
Wrench,
|
|
42
|
+
Map,
|
|
43
|
+
GitBranch,
|
|
44
|
+
Home,
|
|
45
|
+
TrendingUp,
|
|
46
|
+
Clock,
|
|
47
|
+
Maximize2
|
|
48
|
+
} from 'lucide-react';
|
|
49
|
+
import type { Plugin } from 'unified';
|
|
50
|
+
import { visit } from 'unist-util-visit';
|
|
51
|
+
import type { Html, Root } from 'mdast';
|
|
52
|
+
|
|
53
|
+
const remarkStripHtmlComments: Plugin<[], Root> = () => (tree: Root) => {
|
|
54
|
+
visit(tree, 'html', (node: Html) => {
|
|
55
|
+
if (typeof node.value === 'string' && node.value.trim().startsWith('<!--')) {
|
|
56
|
+
node.value = '';
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Icon mapping for sub-specs
|
|
62
|
+
const SUB_SPEC_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
63
|
+
'FileText': FileText,
|
|
64
|
+
'Palette': Palette,
|
|
65
|
+
'Code': Code,
|
|
66
|
+
'TestTube': TestTube,
|
|
67
|
+
'CheckSquare': CheckSquare,
|
|
68
|
+
'Wrench': Wrench,
|
|
69
|
+
'Map': Map,
|
|
70
|
+
'GitBranch': GitBranch,
|
|
71
|
+
'Home': Home,
|
|
72
|
+
'TrendingUp': TrendingUp,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
interface SpecDetailClientProps {
|
|
76
|
+
initialSpec: SpecWithMetadata;
|
|
77
|
+
initialSubSpec?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// SWR fetcher with error handling
|
|
81
|
+
const fetcher = (url: string) => fetch(url).then((res) => {
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
throw new Error(`HTTP error! status: ${res.status}`);
|
|
84
|
+
}
|
|
85
|
+
return res.json();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export function SpecDetailClient({ initialSpec, initialSubSpec }: SpecDetailClientProps) {
|
|
89
|
+
const router = useRouter();
|
|
90
|
+
const searchParams = useSearchParams();
|
|
91
|
+
const currentSubSpec = searchParams.get('subspec') || initialSubSpec;
|
|
92
|
+
const [timelineDialogOpen, setTimelineDialogOpen] = React.useState(false);
|
|
93
|
+
const [dependenciesDialogOpen, setDependenciesDialogOpen] = React.useState(false);
|
|
94
|
+
|
|
95
|
+
// Use SWR for client-side caching with the initial spec as fallback
|
|
96
|
+
const { data: specData, error, isLoading } = useSWR<{ spec: SpecWithMetadata }>(
|
|
97
|
+
`/api/specs/${initialSpec.specNumber || initialSpec.id}`,
|
|
98
|
+
fetcher,
|
|
99
|
+
{
|
|
100
|
+
fallbackData: { spec: initialSpec },
|
|
101
|
+
revalidateOnFocus: false,
|
|
102
|
+
dedupingInterval: 5000, // Don't refetch within 5 seconds
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Fetch complete dependency graph when dialog opens
|
|
107
|
+
const { data: dependencyGraphData } = useSWR<{
|
|
108
|
+
current: any;
|
|
109
|
+
dependsOn: any[];
|
|
110
|
+
requiredBy: any[];
|
|
111
|
+
related: any[];
|
|
112
|
+
}>(
|
|
113
|
+
dependenciesDialogOpen ? `/api/specs/${initialSpec.specNumber || initialSpec.id}/dependency-graph` : null,
|
|
114
|
+
fetcher,
|
|
115
|
+
{
|
|
116
|
+
revalidateOnFocus: false,
|
|
117
|
+
dedupingInterval: 60000, // Cache for 1 minute
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const spec = specData?.spec || initialSpec;
|
|
122
|
+
const tags = spec.tags || [];
|
|
123
|
+
const updatedRelative = spec.updatedAt ? formatRelativeTime(spec.updatedAt) : 'N/A';
|
|
124
|
+
const relationships = spec.relationships;
|
|
125
|
+
|
|
126
|
+
// Use complete graph if available, otherwise fall back to basic relationships
|
|
127
|
+
const completeRelationships = dependencyGraphData
|
|
128
|
+
? {
|
|
129
|
+
dependsOn: dependencyGraphData.dependsOn.map(s => s.specName),
|
|
130
|
+
requiredBy: dependencyGraphData.requiredBy.map(s => s.specName),
|
|
131
|
+
related: dependencyGraphData.related.map(s => s.specName),
|
|
132
|
+
}
|
|
133
|
+
: relationships;
|
|
134
|
+
|
|
135
|
+
const hasRelationships = Boolean(
|
|
136
|
+
relationships && ((relationships.dependsOn?.length ?? 0) > 0 || (relationships.related?.length ?? 0) > 0)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
React.useEffect(() => {
|
|
140
|
+
if (!hasRelationships) {
|
|
141
|
+
setDependenciesDialogOpen(false);
|
|
142
|
+
}
|
|
143
|
+
}, [hasRelationships]);
|
|
144
|
+
|
|
145
|
+
// Extract H1 title from markdown content
|
|
146
|
+
const h1Title = extractH1Title(spec.contentMd);
|
|
147
|
+
const displayTitle = h1Title || spec.title || spec.specName;
|
|
148
|
+
|
|
149
|
+
// Get content to display (main or sub-spec)
|
|
150
|
+
let displayContent = spec.contentMd;
|
|
151
|
+
|
|
152
|
+
if (currentSubSpec && spec.subSpecs) {
|
|
153
|
+
const subSpecData = spec.subSpecs.find(s => s.file === currentSubSpec);
|
|
154
|
+
if (subSpecData) {
|
|
155
|
+
displayContent = subSpecData.content;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Format dates
|
|
160
|
+
const formatDate = (date: Date | string | number | null) => {
|
|
161
|
+
if (!date) return 'N/A';
|
|
162
|
+
return new Date(date).toLocaleDateString('en-US', {
|
|
163
|
+
year: 'numeric',
|
|
164
|
+
month: 'short',
|
|
165
|
+
day: 'numeric'
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Handle sub-spec switching with optimistic UI (instant, no network)
|
|
170
|
+
const handleSubSpecSwitch = (file: string | null) => {
|
|
171
|
+
const newUrl = file
|
|
172
|
+
? `/specs/${spec.specNumber || spec.id}?subspec=${file}`
|
|
173
|
+
: `/specs/${spec.specNumber || spec.id}`;
|
|
174
|
+
|
|
175
|
+
// Use shallow routing to avoid full page reload
|
|
176
|
+
router.push(newUrl, { scroll: false });
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<>
|
|
181
|
+
{/* Compact Header - sticky on desktop, static on mobile */}
|
|
182
|
+
<header className="lg:sticky lg:top-14 lg:z-20 border-b bg-card">
|
|
183
|
+
<div className="px-3 sm:px-6 py-3 sm:py-4">
|
|
184
|
+
{/* Line 1: Spec number + H1 Title */}
|
|
185
|
+
<h1 className="text-xl sm:text-2xl font-bold tracking-tight mb-2 sm:mb-3">
|
|
186
|
+
{spec.specNumber && (
|
|
187
|
+
<span className="text-muted-foreground">#{spec.specNumber.toString().padStart(3, '0')} </span>
|
|
188
|
+
)}
|
|
189
|
+
{displayTitle}
|
|
190
|
+
</h1>
|
|
191
|
+
|
|
192
|
+
{/* Line 2: Status, Priority, Tags, Actions */}
|
|
193
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
194
|
+
<StatusBadge status={spec.status || 'planned'} />
|
|
195
|
+
<PriorityBadge priority={spec.priority || 'medium'} />
|
|
196
|
+
|
|
197
|
+
{tags.length > 0 && (
|
|
198
|
+
<>
|
|
199
|
+
<div className="h-4 w-px bg-border mx-1 hidden sm:block" />
|
|
200
|
+
<div className="flex flex-wrap gap-1">
|
|
201
|
+
{tags.slice(0, 5).map((tag: string) => (
|
|
202
|
+
<Badge key={tag} variant="secondary" className="text-xs">
|
|
203
|
+
{tag}
|
|
204
|
+
</Badge>
|
|
205
|
+
))}
|
|
206
|
+
{tags.length > 5 && (
|
|
207
|
+
<Badge variant="secondary" className="text-xs">
|
|
208
|
+
+{tags.length - 5} more
|
|
209
|
+
</Badge>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
{/* Line 3: Small metadata row */}
|
|
217
|
+
<div className="flex flex-wrap gap-2 sm:gap-4 text-xs text-muted-foreground mt-2 sm:mt-3">
|
|
218
|
+
<span className="hidden sm:inline">Created: {formatDate(spec.createdAt)}</span>
|
|
219
|
+
<span className="hidden sm:inline">•</span>
|
|
220
|
+
<span>
|
|
221
|
+
Updated: {formatDate(spec.updatedAt)}
|
|
222
|
+
{spec.updatedAt && (
|
|
223
|
+
<span className="ml-1 text-[11px] text-muted-foreground/80">({updatedRelative})</span>
|
|
224
|
+
)}
|
|
225
|
+
</span>
|
|
226
|
+
<span className="hidden sm:inline">•</span>
|
|
227
|
+
<span className="hidden md:inline">Name: {spec.specName}</span>
|
|
228
|
+
{spec.assignee && (
|
|
229
|
+
<>
|
|
230
|
+
<span className="hidden sm:inline">•</span>
|
|
231
|
+
<span className="hidden sm:inline">Assignee: {spec.assignee}</span>
|
|
232
|
+
</>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div className="flex flex-wrap items-center gap-2 mt-3">
|
|
237
|
+
<Dialog open={timelineDialogOpen} onOpenChange={setTimelineDialogOpen}>
|
|
238
|
+
<DialogTrigger asChild>
|
|
239
|
+
<Button
|
|
240
|
+
type="button"
|
|
241
|
+
variant="outline"
|
|
242
|
+
size="sm"
|
|
243
|
+
className="h-8 rounded-full border px-3 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground"
|
|
244
|
+
>
|
|
245
|
+
<Clock className="mr-1.5 h-3.5 w-3.5" />
|
|
246
|
+
View Timeline
|
|
247
|
+
<Maximize2 className="ml-1.5 h-3.5 w-3.5" />
|
|
248
|
+
</Button>
|
|
249
|
+
</DialogTrigger>
|
|
250
|
+
<DialogContent className="w-[min(900px,90vw)] max-w-3xl max-h-[90vh] overflow-y-auto">
|
|
251
|
+
<DialogHeader>
|
|
252
|
+
<DialogTitle>Spec Timeline</DialogTitle>
|
|
253
|
+
<DialogDescription>Created, updated, and completion milestones.</DialogDescription>
|
|
254
|
+
</DialogHeader>
|
|
255
|
+
<div className="rounded-xl border border-border bg-muted/30 p-4">
|
|
256
|
+
<SpecTimeline
|
|
257
|
+
createdAt={spec.createdAt}
|
|
258
|
+
updatedAt={spec.updatedAt}
|
|
259
|
+
completedAt={spec.completedAt}
|
|
260
|
+
status={spec.status || 'planned'}
|
|
261
|
+
/>
|
|
262
|
+
</div>
|
|
263
|
+
</DialogContent>
|
|
264
|
+
</Dialog>
|
|
265
|
+
|
|
266
|
+
<Dialog open={dependenciesDialogOpen} onOpenChange={setDependenciesDialogOpen}>
|
|
267
|
+
<DialogTrigger asChild>
|
|
268
|
+
<Button
|
|
269
|
+
type="button"
|
|
270
|
+
variant="outline"
|
|
271
|
+
size="sm"
|
|
272
|
+
disabled={!hasRelationships}
|
|
273
|
+
className={cn(
|
|
274
|
+
'h-8 rounded-full border px-3 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground',
|
|
275
|
+
!hasRelationships && 'cursor-not-allowed opacity-50'
|
|
276
|
+
)}
|
|
277
|
+
>
|
|
278
|
+
<GitBranch className="mr-1.5 h-3.5 w-3.5" />
|
|
279
|
+
View Dependencies
|
|
280
|
+
<Maximize2 className="ml-1.5 h-3.5 w-3.5" />
|
|
281
|
+
</Button>
|
|
282
|
+
</DialogTrigger>
|
|
283
|
+
<DialogContent className="flex h-[85vh] w-[min(1200px,95vw)] max-w-6xl flex-col gap-4 overflow-hidden">
|
|
284
|
+
<DialogHeader>
|
|
285
|
+
<DialogTitle>Dependency Graph</DialogTitle>
|
|
286
|
+
<DialogDescription>
|
|
287
|
+
Precedence requirements and connected specs rendered with automatic layout.
|
|
288
|
+
</DialogDescription>
|
|
289
|
+
</DialogHeader>
|
|
290
|
+
<div className="min-h-0 flex-1">
|
|
291
|
+
{completeRelationships && (
|
|
292
|
+
<SpecDependencyGraph
|
|
293
|
+
relationships={completeRelationships}
|
|
294
|
+
specNumber={spec.specNumber}
|
|
295
|
+
specTitle={displayTitle}
|
|
296
|
+
/>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
</DialogContent>
|
|
300
|
+
</Dialog>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{/* Horizontal Tabs for Sub-specs (only if sub-specs exist) */}
|
|
305
|
+
{spec.subSpecs && spec.subSpecs.length > 0 && (
|
|
306
|
+
<div className="border-t bg-muted/30">
|
|
307
|
+
<div className="px-3 sm:px-6 overflow-x-auto">
|
|
308
|
+
<div className="flex gap-1 py-2 min-w-max">
|
|
309
|
+
{/* Overview tab (README.md) */}
|
|
310
|
+
<button
|
|
311
|
+
onClick={() => handleSubSpecSwitch(null)}
|
|
312
|
+
className={`flex items-center gap-2 px-3 sm:px-4 py-2 text-xs sm:text-sm font-medium rounded-md whitespace-nowrap transition-colors ${
|
|
313
|
+
!currentSubSpec
|
|
314
|
+
? 'bg-background text-foreground shadow-sm'
|
|
315
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
|
316
|
+
}`}
|
|
317
|
+
>
|
|
318
|
+
<Home className="h-4 w-4" />
|
|
319
|
+
<span className="hidden sm:inline">Overview</span>
|
|
320
|
+
</button>
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
{/* Sub-spec tabs */}
|
|
324
|
+
{spec.subSpecs.map((subSpec) => {
|
|
325
|
+
const Icon = SUB_SPEC_ICONS[subSpec.iconName] || FileText;
|
|
326
|
+
return (
|
|
327
|
+
<button
|
|
328
|
+
key={subSpec.file}
|
|
329
|
+
onClick={() => handleSubSpecSwitch(subSpec.file)}
|
|
330
|
+
className={`flex items-center gap-2 px-3 sm:px-4 py-2 text-xs sm:text-sm font-medium rounded-md whitespace-nowrap transition-colors ${
|
|
331
|
+
currentSubSpec === subSpec.file
|
|
332
|
+
? 'bg-background text-foreground shadow-sm'
|
|
333
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
|
334
|
+
}`}
|
|
335
|
+
>
|
|
336
|
+
<Icon className={`h-4 w-4 ${subSpec.color}`} />
|
|
337
|
+
<span className="hidden sm:inline">{subSpec.name}</span>
|
|
338
|
+
</button>
|
|
339
|
+
);
|
|
340
|
+
})}
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
</header>
|
|
346
|
+
|
|
347
|
+
{/* Main content (full width) */}
|
|
348
|
+
<main className="px-3 sm:px-6 py-4 sm:py-8">
|
|
349
|
+
<div className="space-y-6">
|
|
350
|
+
{isLoading && <div className="text-sm text-muted-foreground">Loading...</div>}
|
|
351
|
+
{error && <div className="text-sm text-destructive">Error loading spec</div>}
|
|
352
|
+
|
|
353
|
+
<article className="prose prose-slate dark:prose-invert max-w-none prose-sm sm:prose-base">
|
|
354
|
+
<ReactMarkdown
|
|
355
|
+
remarkPlugins={[remarkGfm, remarkStripHtmlComments]}
|
|
356
|
+
rehypePlugins={[rehypeHighlight, rehypeSlug]}
|
|
357
|
+
components={{
|
|
358
|
+
a: (props) => <MarkdownLink {...props} currentSpecNumber={spec.specNumber || undefined} />,
|
|
359
|
+
}}
|
|
360
|
+
>
|
|
361
|
+
{displayContent}
|
|
362
|
+
</ReactMarkdown>
|
|
363
|
+
</article>
|
|
364
|
+
</div>
|
|
365
|
+
</main>
|
|
366
|
+
|
|
367
|
+
{/* Floating action buttons */}
|
|
368
|
+
<TableOfContents content={displayContent} />
|
|
369
|
+
<BackToTop />
|
|
370
|
+
</>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { SpecsNavSidebar } from '@/components/specs-nav-sidebar';
|
|
4
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
5
|
+
import { useSpecsSidebarState } from '@/lib/stores/specs-sidebar-store';
|
|
6
|
+
|
|
7
|
+
export function SpecDetailLoadingShell() {
|
|
8
|
+
const sidebarState = useSpecsSidebarState();
|
|
9
|
+
const cachedSpecs = sidebarState.specs;
|
|
10
|
+
const activeSpecId = sidebarState.activeSpecId || '';
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="flex min-h-[calc(100vh-3.5rem)] w-full min-w-0">
|
|
14
|
+
<SpecsNavSidebar initialSpecs={cachedSpecs} currentSpecId={activeSpecId} />
|
|
15
|
+
|
|
16
|
+
<div className="flex-1 min-w-0 px-4 py-6 sm:px-8 sm:py-10">
|
|
17
|
+
<div className="space-y-6">
|
|
18
|
+
<div className="space-y-3">
|
|
19
|
+
<Skeleton className="h-7 w-56" />
|
|
20
|
+
<div className="flex flex-wrap gap-3">
|
|
21
|
+
<Skeleton className="h-6 w-24" />
|
|
22
|
+
<Skeleton className="h-6 w-24" />
|
|
23
|
+
<Skeleton className="h-6 w-20" />
|
|
24
|
+
</div>
|
|
25
|
+
<Skeleton className="h-4 w-64" />
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div className="space-y-4">
|
|
29
|
+
<Skeleton className="h-5 w-40" />
|
|
30
|
+
<Skeleton className="h-32 w-full" />
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div className="space-y-4">
|
|
34
|
+
{[...Array(6)].map((_, idx) => (
|
|
35
|
+
<Skeleton key={idx} className="h-4 w-full" />
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client wrapper for spec detail page with prefetching support
|
|
3
|
+
* Phase 2: Tier 2 - Hybrid Rendering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
import { SpecsNavSidebar } from '@/components/specs-nav-sidebar';
|
|
10
|
+
import { SpecDetailClient } from '@/components/spec-detail-client';
|
|
11
|
+
import { primeSpecsSidebar, setActiveSidebarSpec } from '@/lib/stores/specs-sidebar-store';
|
|
12
|
+
import type { SpecWithMetadata, SidebarSpec } from '@/types/specs';
|
|
13
|
+
import type { ParsedSpec } from '@/lib/db/service-queries';
|
|
14
|
+
|
|
15
|
+
interface SpecDetailWrapperProps {
|
|
16
|
+
spec: SpecWithMetadata;
|
|
17
|
+
allSpecs: ParsedSpec[];
|
|
18
|
+
currentSubSpec?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function SpecDetailWrapper({ spec, allSpecs, currentSubSpec }: SpecDetailWrapperProps) {
|
|
22
|
+
const sidebarSpecs = React.useMemo<SidebarSpec[]>(() => (
|
|
23
|
+
allSpecs.map((item) => ({
|
|
24
|
+
id: item.id,
|
|
25
|
+
specNumber: item.specNumber,
|
|
26
|
+
title: item.title,
|
|
27
|
+
specName: item.specName,
|
|
28
|
+
status: item.status,
|
|
29
|
+
priority: item.priority,
|
|
30
|
+
tags: item.tags,
|
|
31
|
+
contentMd: item.contentMd,
|
|
32
|
+
updatedAt: item.updatedAt,
|
|
33
|
+
subSpecsCount: ('subSpecsCount' in item) ? (item as any).subSpecsCount : undefined,
|
|
34
|
+
}))
|
|
35
|
+
), [allSpecs]);
|
|
36
|
+
|
|
37
|
+
// Prime sidebar store with latest metadata (only publishes when signature changes)
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
primeSpecsSidebar(sidebarSpecs);
|
|
40
|
+
}, [sidebarSpecs]);
|
|
41
|
+
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
setActiveSidebarSpec(spec.id);
|
|
44
|
+
}, [spec.id]);
|
|
45
|
+
|
|
46
|
+
// Prefetch spec data on hover
|
|
47
|
+
const handleSpecPrefetch = React.useCallback((specId: string) => {
|
|
48
|
+
fetch(`/api/specs/${specId}`).catch(() => {
|
|
49
|
+
// Prefetch is opportunistic, ignore failures
|
|
50
|
+
});
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex min-h-[calc(100vh-3.5rem)] w-full min-w-0">
|
|
55
|
+
<SpecsNavSidebar
|
|
56
|
+
initialSpecs={sidebarSpecs}
|
|
57
|
+
currentSpecId={spec.id}
|
|
58
|
+
currentSubSpec={currentSubSpec}
|
|
59
|
+
onSpecHover={handleSpecPrefetch}
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
<div className="flex-1 min-w-0">
|
|
63
|
+
<SpecDetailClient
|
|
64
|
+
initialSpec={spec}
|
|
65
|
+
initialSubSpec={currentSubSpec}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata card component with icons for spec details
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Calendar, User, Tag, GitBranch, ExternalLink } from 'lucide-react';
|
|
6
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
7
|
+
import { Badge } from '@/components/ui/badge';
|
|
8
|
+
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
9
|
+
import { StatusBadge } from '@/components/status-badge';
|
|
10
|
+
import { PriorityBadge } from '@/components/priority-badge';
|
|
11
|
+
import { formatDate, formatRelativeTime } from '@/lib/date-utils';
|
|
12
|
+
import type { Spec } from '@/lib/db/schema';
|
|
13
|
+
|
|
14
|
+
interface SpecMetadataProps {
|
|
15
|
+
spec: Spec & { tags: string[] | null };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SpecMetadata({ spec }: SpecMetadataProps) {
|
|
19
|
+
return (
|
|
20
|
+
<Card>
|
|
21
|
+
<CardContent className="pt-6">
|
|
22
|
+
<dl className="grid grid-cols-2 gap-4">
|
|
23
|
+
{/* Status */}
|
|
24
|
+
<div>
|
|
25
|
+
<dt className="text-sm font-medium text-muted-foreground flex items-center gap-1.5 mb-1">
|
|
26
|
+
Status
|
|
27
|
+
</dt>
|
|
28
|
+
<dd>
|
|
29
|
+
<StatusBadge status={spec.status || 'planned'} />
|
|
30
|
+
</dd>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{/* Priority */}
|
|
34
|
+
<div>
|
|
35
|
+
<dt className="text-sm font-medium text-muted-foreground flex items-center gap-1.5 mb-1">
|
|
36
|
+
Priority
|
|
37
|
+
</dt>
|
|
38
|
+
<dd>
|
|
39
|
+
<PriorityBadge priority={spec.priority || 'medium'} />
|
|
40
|
+
</dd>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Created */}
|
|
44
|
+
<div>
|
|
45
|
+
<dt className="text-sm font-medium text-muted-foreground flex items-center gap-1.5 mb-1">
|
|
46
|
+
<Calendar className="h-4 w-4" />
|
|
47
|
+
Created
|
|
48
|
+
</dt>
|
|
49
|
+
<dd className="text-sm">
|
|
50
|
+
{formatDate(spec.createdAt)}
|
|
51
|
+
{spec.createdAt && (
|
|
52
|
+
<span className="text-muted-foreground ml-1">
|
|
53
|
+
({formatRelativeTime(spec.createdAt)})
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
</dd>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* Updated */}
|
|
60
|
+
<div>
|
|
61
|
+
<dt className="text-sm font-medium text-muted-foreground flex items-center gap-1.5 mb-1">
|
|
62
|
+
<Calendar className="h-4 w-4" />
|
|
63
|
+
Updated
|
|
64
|
+
</dt>
|
|
65
|
+
<dd className="text-sm">
|
|
66
|
+
{formatDate(spec.updatedAt)}
|
|
67
|
+
{spec.updatedAt && (
|
|
68
|
+
<span className="text-muted-foreground ml-1">
|
|
69
|
+
({formatRelativeTime(spec.updatedAt)})
|
|
70
|
+
</span>
|
|
71
|
+
)}
|
|
72
|
+
</dd>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Assignee */}
|
|
76
|
+
{spec.assignee && (
|
|
77
|
+
<div>
|
|
78
|
+
<dt className="text-sm font-medium text-muted-foreground flex items-center gap-1.5 mb-1">
|
|
79
|
+
<User className="h-4 w-4" />
|
|
80
|
+
Assignee
|
|
81
|
+
</dt>
|
|
82
|
+
<dd>
|
|
83
|
+
<div className="flex items-center gap-2">
|
|
84
|
+
<Avatar className="h-6 w-6">
|
|
85
|
+
<AvatarFallback className="text-xs">
|
|
86
|
+
{spec.assignee.substring(0, 2).toUpperCase()}
|
|
87
|
+
</AvatarFallback>
|
|
88
|
+
</Avatar>
|
|
89
|
+
<span className="text-sm">{spec.assignee}</span>
|
|
90
|
+
</div>
|
|
91
|
+
</dd>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
{/* Tags */}
|
|
96
|
+
{spec.tags && spec.tags.length > 0 && (
|
|
97
|
+
<div className={spec.assignee ? '' : 'col-span-2'}>
|
|
98
|
+
<dt className="text-sm font-medium text-muted-foreground flex items-center gap-1.5 mb-1">
|
|
99
|
+
<Tag className="h-4 w-4" />
|
|
100
|
+
Tags
|
|
101
|
+
</dt>
|
|
102
|
+
<dd className="flex gap-1 flex-wrap">
|
|
103
|
+
{spec.tags.map((tag) => (
|
|
104
|
+
<Badge key={tag} variant="outline" className="text-xs">
|
|
105
|
+
{tag}
|
|
106
|
+
</Badge>
|
|
107
|
+
))}
|
|
108
|
+
</dd>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{/* GitHub URL */}
|
|
113
|
+
{spec.githubUrl && (
|
|
114
|
+
<div className="col-span-2">
|
|
115
|
+
<dt className="text-sm font-medium text-muted-foreground flex items-center gap-1.5 mb-1">
|
|
116
|
+
<GitBranch className="h-4 w-4" />
|
|
117
|
+
Source
|
|
118
|
+
</dt>
|
|
119
|
+
<dd>
|
|
120
|
+
<a
|
|
121
|
+
href={spec.githubUrl}
|
|
122
|
+
target="_blank"
|
|
123
|
+
rel="noopener noreferrer"
|
|
124
|
+
className="text-sm text-primary hover:underline flex items-center gap-1"
|
|
125
|
+
>
|
|
126
|
+
View on GitHub
|
|
127
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
128
|
+
</a>
|
|
129
|
+
</dd>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</dl>
|
|
133
|
+
</CardContent>
|
|
134
|
+
</Card>
|
|
135
|
+
);
|
|
136
|
+
}
|