@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,369 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import dagre from '@dagrejs/dagre';
6
+ import ReactFlow, {
7
+ Background,
8
+ Controls,
9
+ Edge,
10
+ Handle,
11
+ MarkerType,
12
+ Node,
13
+ NodeProps,
14
+ Position,
15
+ ReactFlowInstance,
16
+ } from 'reactflow';
17
+ import 'reactflow/dist/style.css';
18
+ import type { SpecRelationships } from '@/types/specs';
19
+ import { cn } from '@/lib/utils';
20
+
21
+ const NODE_WIDTH = 280;
22
+ const NODE_HEIGHT = 110;
23
+ const precedenceColor = '#f59e0b';
24
+ const relatedColor = '#38bdf8';
25
+ const requiredByColor = '#ef4444'; // Red color for downstream dependents
26
+
27
+ type GraphTone = 'precedence' | 'related' | 'current' | 'required-by';
28
+
29
+ interface SpecNodeData {
30
+ label: string;
31
+ badge: string;
32
+ subtitle?: string;
33
+ tone: GraphTone;
34
+ href?: string;
35
+ interactive?: boolean;
36
+ }
37
+
38
+ const toneClasses: Record<GraphTone, string> = {
39
+ current: 'border-primary/70 bg-primary/5 text-foreground',
40
+ precedence: 'border-amber-400/70 bg-amber-400/10 text-amber-900 dark:text-amber-200',
41
+ related: 'border-sky-400/70 bg-sky-400/10 text-sky-900 dark:text-sky-200',
42
+ 'required-by': 'border-red-400/70 bg-red-400/10 text-red-900 dark:text-red-200',
43
+ };
44
+
45
+ const dagreConfig: dagre.GraphLabel = {
46
+ rankdir: 'LR',
47
+ align: 'UL',
48
+ nodesep: 60,
49
+ ranksep: 120,
50
+ marginx: 40,
51
+ marginy: 40,
52
+ };
53
+
54
+ const SpecNode = React.memo(function SpecNode({ data }: NodeProps<SpecNodeData>) {
55
+ return (
56
+ <div
57
+ className={cn(
58
+ 'flex w-[280px] flex-col gap-1.5 rounded-xl border-2 px-5 py-4 text-base shadow-md transition-colors',
59
+ toneClasses[data.tone],
60
+ data.interactive && 'cursor-pointer hover:border-primary/70 hover:shadow-lg'
61
+ )}
62
+ >
63
+ <Handle type="target" position={Position.Left} className="opacity-0" />
64
+ <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground/70">
65
+ {data.badge}
66
+ </span>
67
+ <span className="text-base font-semibold leading-snug">{data.label}</span>
68
+ {data.subtitle && (
69
+ <span className="text-sm text-muted-foreground/80">{data.subtitle}</span>
70
+ )}
71
+ <Handle type="source" position={Position.Right} className="opacity-0" />
72
+ </div>
73
+ );
74
+ });
75
+ SpecNode.displayName = 'SpecNode';
76
+
77
+ const nodeTypes = {
78
+ specNode: SpecNode,
79
+ };
80
+
81
+ interface SpecDependencyGraphProps {
82
+ relationships: SpecRelationships;
83
+ specNumber?: number | null;
84
+ specTitle: string;
85
+ }
86
+
87
+ interface GraphPayload {
88
+ nodes: Node<SpecNodeData>[];
89
+ edges: Edge[];
90
+ }
91
+
92
+ function formatRelationshipLabel(value: string) {
93
+ const trimmed = value.trim();
94
+ if (!trimmed) return 'Unknown Spec';
95
+ const match = trimmed.match(/^(\d+)[-_]?(.*)$/);
96
+ if (!match) return trimmed;
97
+ const number = match[1].padStart(3, '0');
98
+ const remainder = match[2]?.replace(/[-_]/g, ' ').trim();
99
+ return remainder ? `#${number} ${remainder}` : `#${number}`;
100
+ }
101
+
102
+ function buildRelationshipHref(value: string) {
103
+ const trimmed = value.trim();
104
+ const match = trimmed.match(/^(\d+)/);
105
+ if (match) {
106
+ return `/specs/${parseInt(match[1], 10)}`;
107
+ }
108
+ return `/specs/${trimmed}`;
109
+ }
110
+
111
+ function nodeId(prefix: string, value: string, index: number) {
112
+ return `${prefix}-${index}-${value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-') || index}`;
113
+ }
114
+
115
+ function layoutGraph(nodes: Node<SpecNodeData>[], edges: Edge[]): GraphPayload {
116
+ const graph = new dagre.graphlib.Graph();
117
+ graph.setGraph(dagreConfig);
118
+ graph.setDefaultEdgeLabel(() => ({}));
119
+
120
+ nodes.forEach((node) => {
121
+ graph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
122
+ });
123
+ edges.forEach((edge) => {
124
+ graph.setEdge(edge.source, edge.target);
125
+ });
126
+
127
+ dagre.layout(graph);
128
+
129
+ const layoutedNodes = nodes.map((node) => {
130
+ const { x, y } = graph.node(node.id);
131
+ return {
132
+ ...node,
133
+ position: { x: x - NODE_WIDTH / 2, y: y - NODE_HEIGHT / 2 },
134
+ };
135
+ });
136
+
137
+ return { nodes: layoutedNodes, edges };
138
+ }
139
+
140
+ function buildGraph(relationships: SpecRelationships, specNumber: number | null | undefined, specTitle: string) {
141
+ const nodes: Node<SpecNodeData>[] = [];
142
+ const edges: Edge[] = [];
143
+ const centerLabel = specNumber ? `#${specNumber.toString().padStart(3, '0')} ${specTitle}` : specTitle;
144
+
145
+ const currentNode: Node<SpecNodeData> = {
146
+ id: 'current-spec',
147
+ type: 'specNode',
148
+ data: {
149
+ label: centerLabel,
150
+ badge: 'Current Spec',
151
+ subtitle: 'This spec',
152
+ tone: 'current',
153
+ interactive: false,
154
+ },
155
+ position: { x: 0, y: 0 },
156
+ draggable: false,
157
+ selectable: false,
158
+ sourcePosition: Position.Right,
159
+ targetPosition: Position.Left,
160
+ };
161
+
162
+ nodes.push(currentNode);
163
+
164
+ // Precedence: Specs this one depends on (upstream, blocking)
165
+ relationships.dependsOn?.forEach((value, index) => {
166
+ const id = nodeId('precedence', value, index);
167
+ nodes.push({
168
+ id,
169
+ type: 'specNode',
170
+ data: {
171
+ label: formatRelationshipLabel(value),
172
+ badge: 'Depends On',
173
+ subtitle: 'Must complete first',
174
+ tone: 'precedence',
175
+ href: buildRelationshipHref(value),
176
+ interactive: true,
177
+ },
178
+ position: { x: 0, y: 0 },
179
+ draggable: false,
180
+ selectable: true,
181
+ sourcePosition: Position.Right,
182
+ targetPosition: Position.Left,
183
+ });
184
+
185
+ edges.push({
186
+ id: `edge-${id}-current`,
187
+ source: id,
188
+ target: currentNode.id,
189
+ type: 'smoothstep',
190
+ markerEnd: {
191
+ type: MarkerType.ArrowClosed,
192
+ color: precedenceColor,
193
+ width: 28,
194
+ height: 28,
195
+ },
196
+ style: {
197
+ stroke: precedenceColor,
198
+ strokeWidth: 3,
199
+ },
200
+ });
201
+ });
202
+
203
+ // Required By: Specs that depend on this one (downstream, blocked)
204
+ relationships.requiredBy?.forEach((value, index) => {
205
+ const id = nodeId('required-by', value, index);
206
+ nodes.push({
207
+ id,
208
+ type: 'specNode',
209
+ data: {
210
+ label: formatRelationshipLabel(value),
211
+ badge: 'Required By',
212
+ subtitle: 'Blocked by this spec',
213
+ tone: 'required-by',
214
+ href: buildRelationshipHref(value),
215
+ interactive: true,
216
+ },
217
+ position: { x: 0, y: 0 },
218
+ draggable: false,
219
+ selectable: true,
220
+ sourcePosition: Position.Right,
221
+ targetPosition: Position.Left,
222
+ });
223
+
224
+ edges.push({
225
+ id: `edge-current-${id}`,
226
+ source: currentNode.id,
227
+ target: id,
228
+ type: 'smoothstep',
229
+ markerEnd: {
230
+ type: MarkerType.ArrowClosed,
231
+ color: requiredByColor,
232
+ width: 28,
233
+ height: 28,
234
+ },
235
+ style: {
236
+ stroke: requiredByColor,
237
+ strokeWidth: 3,
238
+ },
239
+ });
240
+ });
241
+
242
+ // Related: Bidirectional informational connections
243
+ relationships.related?.forEach((value, index) => {
244
+ const id = nodeId('related', value, index);
245
+ nodes.push({
246
+ id,
247
+ type: 'specNode',
248
+ data: {
249
+ label: formatRelationshipLabel(value),
250
+ badge: 'Related',
251
+ subtitle: 'Connected work',
252
+ tone: 'related',
253
+ href: buildRelationshipHref(value),
254
+ interactive: true,
255
+ },
256
+ position: { x: 0, y: 0 },
257
+ draggable: false,
258
+ selectable: true,
259
+ sourcePosition: Position.Right,
260
+ targetPosition: Position.Left,
261
+ });
262
+
263
+ edges.push({
264
+ id: `edge-current-${id}`,
265
+ source: currentNode.id,
266
+ target: id,
267
+ type: 'smoothstep',
268
+ markerEnd: {
269
+ type: MarkerType.ArrowClosed,
270
+ color: relatedColor,
271
+ width: 24,
272
+ height: 24,
273
+ },
274
+ style: {
275
+ stroke: relatedColor,
276
+ strokeWidth: 3,
277
+ strokeDasharray: '10 8',
278
+ },
279
+ });
280
+ });
281
+
282
+ return layoutGraph(nodes, edges);
283
+ }
284
+
285
+ export function SpecDependencyGraph({ relationships, specNumber, specTitle }: SpecDependencyGraphProps) {
286
+ const router = useRouter();
287
+ const [instance, setInstance] = React.useState<ReactFlowInstance | null>(null);
288
+
289
+ const graph = React.useMemo(() => buildGraph(relationships, specNumber, specTitle), [relationships, specNumber, specTitle]);
290
+
291
+ const handleInit = React.useCallback((flowInstance: ReactFlowInstance) => {
292
+ setInstance(flowInstance);
293
+ requestAnimationFrame(() => {
294
+ flowInstance.fitView({ padding: 0.4, duration: 350 });
295
+ });
296
+ }, []);
297
+
298
+ React.useEffect(() => {
299
+ if (!instance) return;
300
+ instance.fitView({ padding: 0.4, duration: 350 });
301
+ }, [instance, graph.nodes]);
302
+
303
+ const handleNodeClick = React.useCallback(
304
+ (_: React.MouseEvent, node: Node<SpecNodeData>) => {
305
+ if (!node?.data) return;
306
+ if (!node.data.href) return;
307
+ router.push(node.data.href);
308
+ },
309
+ [router]
310
+ );
311
+
312
+ return (
313
+ <div className="flex h-full flex-col gap-4">
314
+ <div className="flex flex-wrap items-center justify-between gap-3 text-sm text-muted-foreground">
315
+ <div>
316
+ <p className="text-xs font-semibold uppercase tracking-wide">Dependency Map</p>
317
+ <p className="text-base text-foreground">Explore precedence and connected work</p>
318
+ </div>
319
+ <div className="rounded-full border border-border px-3 py-1.5 text-sm font-medium uppercase tracking-wide">
320
+ React Flow DAG
321
+ </div>
322
+ </div>
323
+
324
+ <div className="flex-1 overflow-hidden rounded-2xl border border-border bg-muted/30">
325
+ <ReactFlow
326
+ nodes={graph.nodes}
327
+ edges={graph.edges}
328
+ nodeTypes={nodeTypes}
329
+ onInit={handleInit}
330
+ className="h-full w-full"
331
+ fitView
332
+ proOptions={{ hideAttribution: true }}
333
+ nodesDraggable={false}
334
+ nodesConnectable={false}
335
+ elementsSelectable
336
+ panOnScroll
337
+ panOnDrag
338
+ zoomOnScroll
339
+ zoomOnPinch
340
+ minZoom={0.4}
341
+ maxZoom={1.6}
342
+ onNodeClick={handleNodeClick}
343
+ >
344
+ <Background gap={24} size={1} color="rgba(148, 163, 184, 0.3)" />
345
+ <Controls showInteractive={false} />
346
+ </ReactFlow>
347
+ </div>
348
+
349
+ <div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
350
+ <span className="inline-flex items-center gap-2 font-medium">
351
+ <span className="inline-block h-2.5 w-8 rounded-full bg-amber-400/80" />
352
+ Depends On → blocks until complete
353
+ </span>
354
+ <span className="inline-flex items-center gap-2 font-medium">
355
+ <span className="inline-block h-2.5 w-8 rounded-full bg-red-400/80" />
356
+ Required By ← blocked by this spec
357
+ </span>
358
+ <span className="inline-flex items-center gap-2 font-medium">
359
+ <span className="inline-block h-2.5 w-8 rounded-full bg-sky-400/80" />
360
+ Related ↔ connected work (bidirectional)
361
+ </span>
362
+ <span className="inline-flex items-center gap-2">
363
+ <span className="inline-block h-2.5 w-8 rounded-full bg-primary/60" />
364
+ Drag to pan • Scroll / pinch to zoom.
365
+ </span>
366
+ </div>
367
+ </div>
368
+ );
369
+ }