@reqord/web 0.1.0

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 (138) hide show
  1. package/LICENSE +661 -0
  2. package/next-env.d.ts +6 -0
  3. package/next.config.ts +7 -0
  4. package/package.json +59 -0
  5. package/postcss.config.mjs +7 -0
  6. package/src/__tests__/components/dashboard/critical-path-display.test.tsx +129 -0
  7. package/src/__tests__/components/dashboard/progress-bar.test.tsx +87 -0
  8. package/src/__tests__/components/dashboard/project-health.test.tsx +57 -0
  9. package/src/__tests__/components/dashboard/warning-alert.test.tsx +75 -0
  10. package/src/__tests__/components/feedback/feedback-client-view.test.tsx +84 -0
  11. package/src/__tests__/components/feedback/feedback-filters.test.tsx +51 -0
  12. package/src/__tests__/components/feedback/feedback-linked-items.test.tsx +131 -0
  13. package/src/__tests__/components/feedback/feedback-list.test.tsx +49 -0
  14. package/src/__tests__/components/feedback/feedback-table.test.tsx +165 -0
  15. package/src/__tests__/components/flags/flag-badge.test.tsx +41 -0
  16. package/src/__tests__/components/flags/flag-list.test.tsx +51 -0
  17. package/src/__tests__/components/gantt/gantt-bar.test.tsx +190 -0
  18. package/src/__tests__/components/gantt/gantt-chart.test.tsx +141 -0
  19. package/src/__tests__/components/gantt/gantt-header.test.tsx +84 -0
  20. package/src/__tests__/components/gantt/gantt-legend.test.tsx +52 -0
  21. package/src/__tests__/components/graph/dag-layout.test.ts +129 -0
  22. package/src/__tests__/components/graph/dependency-graph.test.tsx +94 -0
  23. package/src/__tests__/components/graph/drilldown-breadcrumb.test.tsx +70 -0
  24. package/src/__tests__/components/graph/drilldown-graph.test.tsx +108 -0
  25. package/src/__tests__/components/graph/edge-styles.test.ts +27 -0
  26. package/src/__tests__/components/graph/graph-page-client.test.tsx +124 -0
  27. package/src/__tests__/components/graph/issue-node.test.tsx +173 -0
  28. package/src/__tests__/components/graph/requirement-node.test.tsx +151 -0
  29. package/src/__tests__/components/graph/specification-node.test.tsx +140 -0
  30. package/src/__tests__/components/specification/spec-tabs.test.tsx +153 -0
  31. package/src/__tests__/components/specification/tab-coverage.test.tsx +70 -0
  32. package/src/__tests__/components/specification/tab-design.test.tsx +42 -0
  33. package/src/__tests__/components/specification/tab-history.test.tsx +118 -0
  34. package/src/__tests__/components/specification/tab-issues.test.tsx +126 -0
  35. package/src/__tests__/components/specification/tab-research.test.tsx +42 -0
  36. package/src/__tests__/lib/dashboard-data.test.ts +334 -0
  37. package/src/__tests__/lib/drilldown-graph-data.test.ts +267 -0
  38. package/src/__tests__/lib/gantt-data.test.ts +299 -0
  39. package/src/__tests__/lib/graph-data.test.ts +309 -0
  40. package/src/__tests__/lib/local-feedback-repository.test.ts +74 -0
  41. package/src/__tests__/lib/local-specification-repository.test.ts +194 -0
  42. package/src/__tests__/lib/reqord-root.test.ts +31 -0
  43. package/src/__tests__/lib/specification-file.test.ts +63 -0
  44. package/src/__tests__/lib/tasks-data.test.ts +104 -0
  45. package/src/app/dashboard/loading.tsx +21 -0
  46. package/src/app/dashboard/page.tsx +50 -0
  47. package/src/app/error.tsx +22 -0
  48. package/src/app/feedback/loading.tsx +13 -0
  49. package/src/app/feedback/page.tsx +48 -0
  50. package/src/app/globals.css +2 -0
  51. package/src/app/graph/page.tsx +32 -0
  52. package/src/app/layout.tsx +25 -0
  53. package/src/app/page.tsx +5 -0
  54. package/src/app/requirements/[id]/edit/page.tsx +40 -0
  55. package/src/app/requirements/[id]/loading.tsx +14 -0
  56. package/src/app/requirements/[id]/not-found.tsx +18 -0
  57. package/src/app/requirements/[id]/page.tsx +43 -0
  58. package/src/app/requirements/loading.tsx +13 -0
  59. package/src/app/requirements/new/page.tsx +14 -0
  60. package/src/app/requirements/page.tsx +35 -0
  61. package/src/app/specifications/[id]/loading.tsx +14 -0
  62. package/src/app/specifications/[id]/not-found.tsx +18 -0
  63. package/src/app/specifications/[id]/page.tsx +52 -0
  64. package/src/app/specifications/loading.tsx +13 -0
  65. package/src/app/specifications/page.tsx +42 -0
  66. package/src/components/dashboard/critical-path-display.tsx +76 -0
  67. package/src/components/dashboard/progress-bar.tsx +45 -0
  68. package/src/components/dashboard/progress-section.tsx +57 -0
  69. package/src/components/dashboard/project-health.tsx +35 -0
  70. package/src/components/dashboard/status-card.tsx +27 -0
  71. package/src/components/dashboard/status-cards.tsx +28 -0
  72. package/src/components/dashboard/warning-alert.tsx +33 -0
  73. package/src/components/dashboard/warning-alerts.tsx +24 -0
  74. package/src/components/feedback/feedback-badge.tsx +48 -0
  75. package/src/components/feedback/feedback-client-view.tsx +38 -0
  76. package/src/components/feedback/feedback-filters.tsx +86 -0
  77. package/src/components/feedback/feedback-linked-items.tsx +93 -0
  78. package/src/components/feedback/feedback-list.tsx +40 -0
  79. package/src/components/feedback/feedback-table.tsx +115 -0
  80. package/src/components/gantt/gantt-bar.tsx +65 -0
  81. package/src/components/gantt/gantt-chart.tsx +88 -0
  82. package/src/components/gantt/gantt-constants.ts +15 -0
  83. package/src/components/gantt/gantt-critical-path.tsx +38 -0
  84. package/src/components/gantt/gantt-group.tsx +25 -0
  85. package/src/components/gantt/gantt-header.tsx +47 -0
  86. package/src/components/gantt/gantt-legend.tsx +26 -0
  87. package/src/components/graph/dag-layout.ts +131 -0
  88. package/src/components/graph/dependency-graph.tsx +88 -0
  89. package/src/components/graph/drilldown-breadcrumb.tsx +35 -0
  90. package/src/components/graph/drilldown-graph.tsx +45 -0
  91. package/src/components/graph/edge-styles.ts +16 -0
  92. package/src/components/graph/graph-loader.tsx +25 -0
  93. package/src/components/graph/graph-page-client.tsx +98 -0
  94. package/src/components/graph/issue-node.tsx +46 -0
  95. package/src/components/graph/multi-level-graph.tsx +91 -0
  96. package/src/components/graph/requirement-node.tsx +69 -0
  97. package/src/components/graph/specification-node.tsx +39 -0
  98. package/src/components/requirement/delete-button.tsx +46 -0
  99. package/src/components/requirement/dependency-editor.tsx +79 -0
  100. package/src/components/requirement/markdown-editor.tsx +47 -0
  101. package/src/components/requirement/markdown-renderer.tsx +12 -0
  102. package/src/components/requirement/requirement-detail.tsx +228 -0
  103. package/src/components/requirement/requirement-form.tsx +390 -0
  104. package/src/components/requirement/requirement-table.tsx +203 -0
  105. package/src/components/requirement/requirement-tabs.tsx +65 -0
  106. package/src/components/requirement/success-criteria-editor.tsx +53 -0
  107. package/src/components/specification/spec-detail.tsx +103 -0
  108. package/src/components/specification/spec-tabs.tsx +66 -0
  109. package/src/components/specification/specification-table.tsx +193 -0
  110. package/src/components/specification/tab-coverage.tsx +52 -0
  111. package/src/components/specification/tab-design.tsx +16 -0
  112. package/src/components/specification/tab-history.tsx +61 -0
  113. package/src/components/specification/tab-issues.tsx +111 -0
  114. package/src/components/specification/tab-research.tsx +16 -0
  115. package/src/components/ui/badge.tsx +64 -0
  116. package/src/components/ui/nav.tsx +49 -0
  117. package/src/components/ui/tabs.tsx +39 -0
  118. package/src/lib/actions.ts +222 -0
  119. package/src/lib/dashboard-data.ts +224 -0
  120. package/src/lib/data.ts +21 -0
  121. package/src/lib/drilldown-graph-data.ts +98 -0
  122. package/src/lib/feedback-data.ts +33 -0
  123. package/src/lib/feedback-repository.ts +6 -0
  124. package/src/lib/file-system.ts +167 -0
  125. package/src/lib/gantt-data.ts +168 -0
  126. package/src/lib/get-repository.ts +43 -0
  127. package/src/lib/graph-data.ts +161 -0
  128. package/src/lib/id-generator.ts +23 -0
  129. package/src/lib/local-feedback-repository.ts +36 -0
  130. package/src/lib/local-repository.ts +78 -0
  131. package/src/lib/local-specification-repository.ts +61 -0
  132. package/src/lib/repository.ts +11 -0
  133. package/src/lib/reqord-root.ts +33 -0
  134. package/src/lib/specification-data.ts +28 -0
  135. package/src/lib/specification-file.ts +12 -0
  136. package/src/lib/specification-repository.ts +8 -0
  137. package/src/lib/tasks-data.ts +32 -0
  138. package/tsconfig.json +27 -0
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import React, { useMemo } from "react";
4
+ import { ReactFlow, Background, Controls } from "@xyflow/react";
5
+ import { RequirementNode } from "./requirement-node";
6
+ import { SpecificationNode } from "./specification-node";
7
+ import { IssueNode } from "./issue-node";
8
+ import { buildDrillDownGraphData } from "@/lib/drilldown-graph-data";
9
+ import type { Requirement, Specification, TaskEntry } from "@reqord/shared";
10
+
11
+ interface DrillDownGraphProps {
12
+ requirement: Requirement;
13
+ specifications: Specification[];
14
+ tasks: TaskEntry[];
15
+ }
16
+
17
+ const nodeTypes = {
18
+ requirement: RequirementNode,
19
+ specification: SpecificationNode,
20
+ issue: IssueNode,
21
+ };
22
+
23
+ export function DrillDownGraph({ requirement, specifications, tasks }: DrillDownGraphProps) {
24
+ const { nodes, edges } = useMemo(
25
+ () => buildDrillDownGraphData(requirement, specifications, tasks),
26
+ [requirement, specifications, tasks],
27
+ );
28
+
29
+ return (
30
+ <div className="h-[calc(100vh-10rem)] w-full rounded-lg border border-gray-200 bg-white">
31
+ <ReactFlow
32
+ nodes={nodes}
33
+ edges={edges}
34
+ nodeTypes={nodeTypes}
35
+ fitView
36
+ minZoom={0.1}
37
+ maxZoom={2}
38
+ proOptions={{ hideAttribution: true }}
39
+ >
40
+ <Background />
41
+ <Controls />
42
+ </ReactFlow>
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,16 @@
1
+ export const EDGE_STYLES = {
2
+ dependency: {
3
+ stroke: "#64748b",
4
+ strokeWidth: 2,
5
+ },
6
+ implements: {
7
+ stroke: "#3b82f6",
8
+ strokeWidth: 2,
9
+ strokeDasharray: "5,5",
10
+ },
11
+ tracks: {
12
+ stroke: "#22c55e",
13
+ strokeWidth: 1.5,
14
+ strokeDasharray: "2,2",
15
+ },
16
+ } as const;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+ import type { Requirement } from "@reqord/shared";
5
+
6
+ const DependencyGraph = dynamic(
7
+ () =>
8
+ import("./dependency-graph").then((mod) => mod.DependencyGraph),
9
+ {
10
+ ssr: false,
11
+ loading: () => (
12
+ <div className="animate-pulse h-96 rounded-lg bg-gray-200" />
13
+ ),
14
+ },
15
+ );
16
+
17
+ export function GraphLoader({
18
+ requirements,
19
+ specCountMap = {},
20
+ }: {
21
+ requirements: Requirement[];
22
+ specCountMap?: Record<string, number>;
23
+ }) {
24
+ return <DependencyGraph requirements={requirements} specCountMap={specCountMap} />;
25
+ }
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ import React, { useCallback } from "react";
4
+ import dynamic from "next/dynamic";
5
+ import { useSearchParams, useRouter } from "next/navigation";
6
+ import type { Requirement, Specification, TaskEntry } from "@reqord/shared";
7
+ import { DrillDownBreadcrumb } from "./drilldown-breadcrumb";
8
+
9
+ const DependencyGraph = dynamic(
10
+ () => import("./dependency-graph").then((mod) => mod.DependencyGraph),
11
+ {
12
+ ssr: false,
13
+ loading: () => (
14
+ <div className="animate-pulse h-96 rounded-lg bg-gray-200" />
15
+ ),
16
+ }
17
+ );
18
+
19
+ const DrillDownGraph = dynamic(
20
+ () => import("./drilldown-graph").then((mod) => mod.DrillDownGraph),
21
+ {
22
+ ssr: false,
23
+ loading: () => (
24
+ <div className="animate-pulse h-96 rounded-lg bg-gray-200" />
25
+ ),
26
+ }
27
+ );
28
+
29
+ type GraphPageClientProps = {
30
+ requirements: Requirement[];
31
+ specifications: Specification[];
32
+ specCountMap: Record<string, number>;
33
+ tasks: TaskEntry[];
34
+ };
35
+
36
+ export function GraphPageClient({
37
+ requirements,
38
+ specifications,
39
+ specCountMap,
40
+ tasks,
41
+ }: GraphPageClientProps) {
42
+ const searchParams = useSearchParams();
43
+ const router = useRouter();
44
+ const selectedReqId = searchParams.get("req");
45
+
46
+ const handleRequirementClick = useCallback(
47
+ (reqId: string) => {
48
+ router.push(`/graph?req=${reqId}`);
49
+ },
50
+ [router],
51
+ );
52
+
53
+ const handleBackToOverview = useCallback(() => {
54
+ router.push("/graph");
55
+ }, [router]);
56
+
57
+ if (selectedReqId) {
58
+ const requirement = requirements.find((r) => r.id === selectedReqId);
59
+ if (!requirement) {
60
+ // Invalid req ID → fallback to overview
61
+ return (
62
+ <div>
63
+ <h1 className="mb-4 text-2xl font-bold">Dependency Graph</h1>
64
+ <DependencyGraph
65
+ requirements={requirements}
66
+ specCountMap={specCountMap}
67
+ onRequirementClick={handleRequirementClick}
68
+ />
69
+ </div>
70
+ );
71
+ }
72
+
73
+ const relatedSpecs = specifications.filter(
74
+ (s) => s.requirementId === selectedReqId,
75
+ );
76
+
77
+ return (
78
+ <div>
79
+ <DrillDownBreadcrumb
80
+ requirementTitle={requirement.title}
81
+ onBack={handleBackToOverview}
82
+ />
83
+ <DrillDownGraph requirement={requirement} specifications={relatedSpecs} tasks={tasks} />
84
+ </div>
85
+ );
86
+ }
87
+
88
+ return (
89
+ <div>
90
+ <h1 className="mb-4 text-2xl font-bold">Dependency Graph</h1>
91
+ <DependencyGraph
92
+ requirements={requirements}
93
+ specCountMap={specCountMap}
94
+ onRequirementClick={handleRequirementClick}
95
+ />
96
+ </div>
97
+ );
98
+ }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import React, { memo } from "react";
4
+ import { Handle, Position, type NodeProps } from "@xyflow/react";
5
+
6
+ const STATUS_COLORS: Record<string, string> = {
7
+ open: "bg-yellow-200",
8
+ in_progress: "bg-blue-200",
9
+ closed: "bg-green-200",
10
+ };
11
+
12
+ type IssueNodeData = {
13
+ label: string;
14
+ status: string;
15
+ issueNumber: number;
16
+ issueUrl: string;
17
+ };
18
+
19
+ function IssueNodeComponent({ data }: NodeProps) {
20
+ const nodeData = data as IssueNodeData;
21
+ const bgClass = STATUS_COLORS[nodeData.status] ?? "bg-gray-200";
22
+
23
+ const handleClick = () => {
24
+ if (nodeData.issueUrl) {
25
+ window.open(nodeData.issueUrl, "_blank", "noopener,noreferrer");
26
+ }
27
+ };
28
+
29
+ return (
30
+ <div
31
+ className={`rounded-lg border shadow-sm px-3 py-2 cursor-pointer ${bgClass}`}
32
+ style={{ width: 180 }}
33
+ onClick={handleClick}
34
+ >
35
+ <Handle type="target" position={Position.Left} className="!bg-gray-400" />
36
+ <p className="truncate text-sm font-medium text-gray-900">{nodeData.label}</p>
37
+ <div className="mt-1">
38
+ <span className="inline-block rounded-full bg-white px-2 py-0.5 text-xs text-gray-700">
39
+ {nodeData.status}
40
+ </span>
41
+ </div>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ export const IssueNode = memo(IssueNodeComponent);
@@ -0,0 +1,91 @@
1
+ "use client";
2
+
3
+ import React, { useMemo, useCallback } from "react";
4
+ import {
5
+ ReactFlow,
6
+ Background,
7
+ Controls,
8
+ MiniMap,
9
+ useNodesState,
10
+ useEdgesState,
11
+ type Node,
12
+ type Edge,
13
+ } from "@xyflow/react";
14
+ import "@xyflow/react/dist/style.css";
15
+ import type { MultiLevelGraphData } from "@/lib/graph-data";
16
+ import { RequirementNode } from "./requirement-node";
17
+ import { SpecificationNode } from "./specification-node";
18
+ import { IssueNode } from "./issue-node";
19
+
20
+ const nodeTypes = {
21
+ requirement: RequirementNode,
22
+ specification: SpecificationNode,
23
+ issue: IssueNode,
24
+ };
25
+
26
+ const EDGE_STYLES = {
27
+ dependency: {
28
+ style: { stroke: "#94a3b8", strokeWidth: 2 },
29
+ markerEnd: { type: "arrowclosed" as const, color: "#94a3b8" },
30
+ animated: false,
31
+ },
32
+ implements: {
33
+ style: { stroke: "#6366f1", strokeWidth: 2, strokeDasharray: "5,5" },
34
+ markerEnd: { type: "arrowclosed" as const, color: "#6366f1" },
35
+ animated: false,
36
+ },
37
+ tracks: {
38
+ style: { stroke: "#a855f7", strokeWidth: 2, strokeDasharray: "2,2" },
39
+ markerEnd: { type: "arrowclosed" as const, color: "#a855f7" },
40
+ animated: false,
41
+ },
42
+ };
43
+
44
+ export function MultiLevelGraph({ data }: { data: MultiLevelGraphData }) {
45
+ const { initialNodes, initialEdges } = useMemo(() => {
46
+ const rfNodes: Node[] = data.nodes.map((n) => ({
47
+ id: n.id,
48
+ type: n.type,
49
+ position: n.position,
50
+ data: n.data,
51
+ }));
52
+
53
+ const rfEdges: Edge[] = data.edges.map((e) => {
54
+ const edgeStyle = EDGE_STYLES[e.type];
55
+ return {
56
+ id: e.id,
57
+ source: e.source,
58
+ target: e.target,
59
+ ...edgeStyle,
60
+ };
61
+ });
62
+
63
+ return { initialNodes: rfNodes, initialEdges: rfEdges };
64
+ }, [data]);
65
+
66
+ const [nodes, , onNodesChange] = useNodesState(initialNodes);
67
+ const [edges, , onEdgesChange] = useEdgesState(initialEdges);
68
+
69
+ const onInit = useCallback((instance: { fitView: () => void }) => {
70
+ instance.fitView();
71
+ }, []);
72
+
73
+ return (
74
+ <div className="h-[calc(100vh-10rem)] w-full rounded-lg border border-gray-200 bg-white">
75
+ <ReactFlow
76
+ nodes={nodes}
77
+ edges={edges}
78
+ onNodesChange={onNodesChange}
79
+ onEdgesChange={onEdgesChange}
80
+ onInit={onInit}
81
+ nodeTypes={nodeTypes}
82
+ fitView
83
+ proOptions={{ hideAttribution: true }}
84
+ >
85
+ <Background />
86
+ <Controls />
87
+ <MiniMap nodeStrokeWidth={3} zoomable pannable />
88
+ </ReactFlow>
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import React, { memo, useCallback } from "react";
4
+ import { Handle, Position, type NodeProps } from "@xyflow/react";
5
+ import { useRouter } from "next/navigation";
6
+
7
+ const STATUS_COLORS: Record<string, string> = {
8
+ draft: "border-gray-300 bg-gray-50",
9
+ approved: "border-green-300 bg-green-50",
10
+ implemented: "border-blue-300 bg-blue-50",
11
+ deprecated: "border-red-300 bg-red-50",
12
+ };
13
+
14
+ type RequirementNodeData = {
15
+ label: string;
16
+ status: string;
17
+ priority: string;
18
+ specCount?: number;
19
+ onDrillDown?: (reqId: string) => void;
20
+ };
21
+
22
+ function RequirementNodeComponent({ data, id }: NodeProps) {
23
+ const nodeData = data as RequirementNodeData;
24
+ const borderClass = STATUS_COLORS[nodeData.status] ?? "border-gray-300 bg-white";
25
+ const router = useRouter();
26
+
27
+ const handleBodyClick = useCallback(() => {
28
+ router.push(`/requirements/${id}`);
29
+ }, [router, id]);
30
+
31
+ const handleDrillDown = useCallback(
32
+ (e: React.MouseEvent) => {
33
+ e.stopPropagation();
34
+ nodeData.onDrillDown?.(id);
35
+ },
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
37
+ [nodeData.onDrillDown, id],
38
+ );
39
+
40
+ const handleButtonMouseDown = useCallback((e: React.MouseEvent) => {
41
+ e.stopPropagation();
42
+ }, []);
43
+
44
+ return (
45
+ <div
46
+ className={`cursor-pointer rounded-lg border-2 px-3 py-2 shadow-sm ${borderClass}`}
47
+ style={{ width: 220 }}
48
+ onClick={handleBodyClick}
49
+ >
50
+ <Handle type="target" position={Position.Left} className="!bg-gray-400" />
51
+ <p className="truncate text-xs font-mono text-gray-500">{id}</p>
52
+ <p className="mt-0.5 truncate text-sm font-medium text-gray-900">
53
+ {nodeData.label}
54
+ </p>
55
+ {nodeData.onDrillDown && nodeData.specCount && nodeData.specCount > 0 ? (
56
+ <button
57
+ className="nodrag mt-1 text-xs text-gray-400"
58
+ onClick={handleDrillDown}
59
+ onMouseDown={handleButtonMouseDown}
60
+ >
61
+ 📄 {nodeData.specCount} spec{nodeData.specCount > 1 ? "s" : ""}
62
+ </button>
63
+ ) : null}
64
+ <Handle type="source" position={Position.Right} className="!bg-gray-400" />
65
+ </div>
66
+ );
67
+ }
68
+
69
+ export const RequirementNode = memo(RequirementNodeComponent);
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import React, { memo } from "react";
4
+ import { Handle, Position, type NodeProps } from "@xyflow/react";
5
+
6
+ const STATUS_COLORS: Record<string, string> = {
7
+ draft: "bg-blue-200",
8
+ approved: "bg-green-200",
9
+ implemented: "bg-emerald-300",
10
+ deprecated: "bg-red-200",
11
+ };
12
+
13
+ type SpecificationNodeData = {
14
+ label: string;
15
+ status: string;
16
+ };
17
+
18
+ function SpecificationNodeComponent({ data, id }: NodeProps) {
19
+ const nodeData = data as SpecificationNodeData;
20
+ const bgClass = STATUS_COLORS[nodeData.status] ?? "bg-gray-200";
21
+
22
+ return (
23
+ <div
24
+ className={`rounded-lg border shadow-sm px-3 py-2 ${bgClass}`}
25
+ style={{ width: 200 }}
26
+ >
27
+ <Handle type="target" position={Position.Left} className="!bg-gray-400" />
28
+ <p className="truncate text-xs font-mono text-gray-900">{id}</p>
29
+ <div className="mt-1">
30
+ <span className="inline-block rounded-full bg-white px-2 py-0.5 text-xs text-gray-700">
31
+ {nodeData.status}
32
+ </span>
33
+ </div>
34
+ <Handle type="source" position={Position.Right} className="!bg-gray-400" />
35
+ </div>
36
+ );
37
+ }
38
+
39
+ export const SpecificationNode = memo(SpecificationNodeComponent);
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { useState, useTransition } from "react";
4
+ import { deleteRequirement } from "@/lib/actions";
5
+
6
+ export function DeleteButton({ id }: { id: string }) {
7
+ const [showConfirm, setShowConfirm] = useState(false);
8
+ const [isPending, startTransition] = useTransition();
9
+
10
+ function handleDelete() {
11
+ startTransition(async () => {
12
+ await deleteRequirement(id);
13
+ });
14
+ }
15
+
16
+ if (showConfirm) {
17
+ return (
18
+ <div className="flex items-center gap-2">
19
+ <span className="text-sm text-red-600">Delete {id}?</span>
20
+ <button
21
+ onClick={handleDelete}
22
+ disabled={isPending}
23
+ className="rounded-md bg-red-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-50"
24
+ >
25
+ {isPending ? "Deleting..." : "Confirm"}
26
+ </button>
27
+ <button
28
+ onClick={() => setShowConfirm(false)}
29
+ disabled={isPending}
30
+ className="rounded-md border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50"
31
+ >
32
+ Cancel
33
+ </button>
34
+ </div>
35
+ );
36
+ }
37
+
38
+ return (
39
+ <button
40
+ onClick={() => setShowConfirm(true)}
41
+ className="rounded-md border border-red-300 px-3 py-1.5 text-sm font-medium text-red-600 hover:bg-red-50"
42
+ >
43
+ Delete
44
+ </button>
45
+ );
46
+ }
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import type { Requirement } from "@reqord/shared";
4
+ import { useMemo } from "react";
5
+
6
+ interface Props {
7
+ label: string;
8
+ selected: string[];
9
+ onChange: (ids: string[]) => void;
10
+ allRequirements: Requirement[];
11
+ excludeId?: string;
12
+ }
13
+
14
+ export function DependencyEditor({
15
+ label,
16
+ selected,
17
+ onChange,
18
+ allRequirements,
19
+ excludeId,
20
+ }: Props) {
21
+ const options = useMemo(
22
+ () => allRequirements.filter((r) => r.id !== excludeId),
23
+ [allRequirements, excludeId],
24
+ );
25
+
26
+ function handleToggle(id: string) {
27
+ if (selected.includes(id)) {
28
+ onChange(selected.filter((s) => s !== id));
29
+ } else {
30
+ onChange([...selected, id]);
31
+ }
32
+ }
33
+
34
+ return (
35
+ <div className="space-y-2">
36
+ <label className="block text-sm font-medium text-gray-700">{label}</label>
37
+ <div className="max-h-40 overflow-y-auto rounded-md border border-gray-300 p-2">
38
+ {options.length === 0 ? (
39
+ <p className="text-sm text-gray-400">No other requirements</p>
40
+ ) : (
41
+ options.map((req) => (
42
+ <label
43
+ key={req.id}
44
+ className="flex items-center gap-2 rounded px-2 py-1 text-sm hover:bg-gray-50"
45
+ >
46
+ <input
47
+ type="checkbox"
48
+ checked={selected.includes(req.id)}
49
+ onChange={() => handleToggle(req.id)}
50
+ className="rounded border-gray-300"
51
+ />
52
+ <span className="font-mono text-gray-500">{req.id}</span>
53
+ <span className="truncate">{req.title}</span>
54
+ </label>
55
+ ))
56
+ )}
57
+ </div>
58
+ {selected.length > 0 ? (
59
+ <div className="flex flex-wrap gap-1">
60
+ {selected.map((id) => (
61
+ <span
62
+ key={id}
63
+ className="inline-flex items-center gap-1 rounded-full bg-blue-100 px-2 py-0.5 text-xs font-mono text-blue-700"
64
+ >
65
+ {id}
66
+ <button
67
+ type="button"
68
+ onClick={() => onChange(selected.filter((s) => s !== id))}
69
+ className="hover:text-blue-900"
70
+ >
71
+ x
72
+ </button>
73
+ </span>
74
+ ))}
75
+ </div>
76
+ ) : null}
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { MarkdownRenderer } from "./markdown-renderer";
5
+
6
+ interface Props {
7
+ value: string;
8
+ onChange: (value: string) => void;
9
+ }
10
+
11
+ export function MarkdownEditor({ value, onChange }: Props) {
12
+ const [showPreview, setShowPreview] = useState(false);
13
+
14
+ return (
15
+ <div className="space-y-2">
16
+ <div className="flex items-center justify-between">
17
+ <label className="block text-sm font-medium text-gray-700">
18
+ Description (Markdown)
19
+ </label>
20
+ <button
21
+ type="button"
22
+ onClick={() => setShowPreview((prev) => !prev)}
23
+ className="text-sm text-blue-600 hover:text-blue-800"
24
+ >
25
+ {showPreview ? "Edit" : "Preview"}
26
+ </button>
27
+ </div>
28
+ {showPreview ? (
29
+ <div className="min-h-[200px] rounded-md border border-gray-300 bg-white p-4">
30
+ {value.trim() ? (
31
+ <MarkdownRenderer content={value} />
32
+ ) : (
33
+ <p className="text-sm text-gray-400">No content</p>
34
+ )}
35
+ </div>
36
+ ) : (
37
+ <textarea
38
+ value={value}
39
+ onChange={(e) => onChange(e.target.value)}
40
+ rows={10}
41
+ className="w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
42
+ placeholder="Enter requirement description in Markdown..."
43
+ />
44
+ )}
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,12 @@
1
+ "use client";
2
+
3
+ import ReactMarkdown from "react-markdown";
4
+ import remarkGfm from "remark-gfm";
5
+
6
+ export function MarkdownRenderer({ content }: { content: string }) {
7
+ return (
8
+ <div className="prose prose-sm max-w-none">
9
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
10
+ </div>
11
+ );
12
+ }