@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,76 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import type { CriticalPathItem } from "@/lib/dashboard-data";
5
+
6
+ type CriticalPathDisplayProps = {
7
+ items: CriticalPathItem[];
8
+ };
9
+
10
+ const INITIAL_DISPLAY_COUNT = 10;
11
+
12
+ export function CriticalPathDisplay({ items }: CriticalPathDisplayProps) {
13
+ const openItems = items.filter((item) => item.status !== "closed");
14
+ const closedItems = items.filter((item) => item.status === "closed");
15
+ const [showClosed, setShowClosed] = useState(false);
16
+
17
+ return (
18
+ <div className="rounded-lg border border-gray-200 bg-white p-6">
19
+ <h2 className="mb-4 text-lg font-semibold text-gray-900">
20
+ Critical Path
21
+ <span className="ml-2 text-sm font-normal text-gray-500">
22
+ {openItems.length} open / {closedItems.length} closed
23
+ </span>
24
+ </h2>
25
+ {/* Show only open items when they exist; closed items are accessible via the expand button below */}
26
+ <div className="space-y-3">
27
+ {(openItems.length > 0
28
+ ? openItems
29
+ : (showClosed ? closedItems : closedItems.slice(0, INITIAL_DISPLAY_COUNT))
30
+ ).map((item) => {
31
+ const isCompleted = item.status === "closed";
32
+ const isPending =
33
+ item.status === "open" || item.status === "in_progress";
34
+
35
+ let textClass = "";
36
+ if (isCompleted) {
37
+ textClass = "line-through";
38
+ } else if (isPending) {
39
+ textClass = "font-bold";
40
+ }
41
+
42
+ return (
43
+ <div
44
+ key={item.issueNumber}
45
+ data-testid="critical-path-item"
46
+ className={`flex items-start gap-3 rounded-md border border-gray-200 p-3 ${textClass}`}
47
+ >
48
+ <div className="flex-1">
49
+ <div className="flex items-center gap-2">
50
+ <span className="text-sm font-medium text-gray-900">
51
+ #{item.issueNumber}
52
+ </span>
53
+ <span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800">
54
+ {item.priority}
55
+ </span>
56
+ </div>
57
+ <p className="mt-1 text-sm text-gray-700">{item.title}</p>
58
+ <p className="mt-1 text-xs text-gray-500">{item.status}</p>
59
+ </div>
60
+ </div>
61
+ );
62
+ })}
63
+ </div>
64
+ {openItems.length === 0 && closedItems.length > INITIAL_DISPLAY_COUNT && (
65
+ <button
66
+ onClick={() => setShowClosed(!showClosed)}
67
+ className="mt-3 text-sm text-blue-600 hover:text-blue-800 hover:underline"
68
+ >
69
+ {showClosed
70
+ ? "Show less"
71
+ : `Show all ${closedItems.length} closed items`}
72
+ </button>
73
+ )}
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ type ProgressBarProps = {
6
+ label: string;
7
+ current: number;
8
+ total: number;
9
+ percentage: number;
10
+ color: string;
11
+ };
12
+
13
+ const COLOR_CLASSES: Record<string, string> = {
14
+ blue: "bg-blue-500",
15
+ purple: "bg-purple-500",
16
+ green: "bg-green-500",
17
+ };
18
+
19
+ export function ProgressBar({
20
+ label,
21
+ current,
22
+ total,
23
+ percentage,
24
+ color,
25
+ }: ProgressBarProps) {
26
+ const bgClass = COLOR_CLASSES[color] ?? "bg-gray-500";
27
+
28
+ return (
29
+ <div className="space-y-2">
30
+ <div className="flex items-center justify-between">
31
+ <span className="text-sm font-medium text-gray-700">{label}</span>
32
+ <span className="text-sm text-gray-600">
33
+ {current}/{total} ({percentage}%)
34
+ </span>
35
+ </div>
36
+ <div className="h-2 overflow-hidden rounded-full bg-gray-200">
37
+ <div
38
+ data-testid="progress-bar-fill"
39
+ className={`h-full ${bgClass} transition-all duration-300`}
40
+ style={{ width: `${percentage}%` }}
41
+ />
42
+ </div>
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,57 @@
1
+ import React from "react";
2
+ import { ProgressBar } from "./progress-bar";
3
+ import type { CategorySummary, IssueSummary } from "@/lib/dashboard-data";
4
+
5
+ type ProgressSectionProps = {
6
+ requirements: CategorySummary;
7
+ specifications: CategorySummary;
8
+ issues: IssueSummary;
9
+ };
10
+
11
+ export function ProgressSection({
12
+ requirements,
13
+ specifications,
14
+ issues,
15
+ }: ProgressSectionProps) {
16
+ const reqApproved = Math.round(requirements.total * requirements.approvalRate);
17
+ const reqPercentage = Math.round(requirements.approvalRate * 100);
18
+
19
+ const specApproved = Math.round(
20
+ specifications.total * specifications.approvalRate
21
+ );
22
+ const specPercentage = Math.round(specifications.approvalRate * 100);
23
+
24
+ const issuesPercentage = Math.round(issues.completionRate * 100);
25
+
26
+ return (
27
+ <div className="grid gap-6 md:grid-cols-3">
28
+ <div className="rounded-lg border border-gray-200 bg-white p-6">
29
+ <ProgressBar
30
+ label="Requirements"
31
+ current={reqApproved}
32
+ total={requirements.total}
33
+ percentage={reqPercentage}
34
+ color="blue"
35
+ />
36
+ </div>
37
+ <div className="rounded-lg border border-gray-200 bg-white p-6">
38
+ <ProgressBar
39
+ label="Specifications"
40
+ current={specApproved}
41
+ total={specifications.total}
42
+ percentage={specPercentage}
43
+ color="purple"
44
+ />
45
+ </div>
46
+ <div className="rounded-lg border border-gray-200 bg-white p-6">
47
+ <ProgressBar
48
+ label="Issues"
49
+ current={issues.completed}
50
+ total={issues.total}
51
+ percentage={issuesPercentage}
52
+ color="green"
53
+ />
54
+ </div>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ type ProjectHealthProps = {
6
+ score: number;
7
+ };
8
+
9
+ export function ProjectHealth({ score }: ProjectHealthProps) {
10
+ const roundedScore = Math.round(score);
11
+
12
+ let colorClass = "text-red-500";
13
+ if (roundedScore >= 80) {
14
+ colorClass = "text-green-500";
15
+ } else if (roundedScore >= 50) {
16
+ colorClass = "text-yellow-500";
17
+ }
18
+
19
+ return (
20
+ <div className="rounded-lg border border-gray-200 bg-white p-6">
21
+ <h2 className="mb-4 text-lg font-semibold text-gray-900">
22
+ Project Health
23
+ </h2>
24
+ <div className="flex items-baseline justify-center">
25
+ <span
26
+ data-testid="health-score"
27
+ className={`text-6xl font-bold ${colorClass}`}
28
+ >
29
+ {roundedScore}
30
+ </span>
31
+ <span className="ml-2 text-2xl text-gray-500">/ 100</span>
32
+ </div>
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import type { StatusBreakdown } from "@/lib/dashboard-data";
3
+
4
+ type StatusCardProps = {
5
+ title: string;
6
+ total: number;
7
+ breakdown: StatusBreakdown;
8
+ };
9
+
10
+ export function StatusCard({ title, total, breakdown }: StatusCardProps) {
11
+ return (
12
+ <div className="rounded-lg border border-gray-200 bg-white p-6">
13
+ <h3 className="text-lg font-semibold text-gray-900">{title}</h3>
14
+ <p className="mt-2 text-3xl font-bold text-gray-900">{total}</p>
15
+ <div className="mt-4 flex flex-wrap gap-2">
16
+ {Object.entries(breakdown).map(([status, count]) => (
17
+ <span
18
+ key={status}
19
+ className="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-800"
20
+ >
21
+ {status}: {count}
22
+ </span>
23
+ ))}
24
+ </div>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+ import { StatusCard } from "./status-card";
3
+ import type { CategorySummary } from "@/lib/dashboard-data";
4
+
5
+ type StatusCardsProps = {
6
+ requirements: CategorySummary;
7
+ specifications: CategorySummary;
8
+ };
9
+
10
+ export function StatusCards({
11
+ requirements,
12
+ specifications,
13
+ }: StatusCardsProps) {
14
+ return (
15
+ <div className="grid gap-6 md:grid-cols-2">
16
+ <StatusCard
17
+ title="Requirements"
18
+ total={requirements.total}
19
+ breakdown={requirements.breakdown}
20
+ />
21
+ <StatusCard
22
+ title="Specifications"
23
+ total={specifications.total}
24
+ breakdown={specifications.breakdown}
25
+ />
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import type { Warning } from "@/lib/dashboard-data";
3
+
4
+ type WarningAlertProps = {
5
+ warning: Warning;
6
+ };
7
+
8
+ export function WarningAlert({ warning }: WarningAlertProps) {
9
+ let borderColor = "border-blue-500";
10
+ let bgColor = "bg-blue-50";
11
+
12
+ if (warning.severity === "error") {
13
+ borderColor = "border-red-500";
14
+ bgColor = "bg-red-50";
15
+ } else if (warning.severity === "warning") {
16
+ borderColor = "border-yellow-500";
17
+ bgColor = "bg-yellow-50";
18
+ }
19
+
20
+ return (
21
+ <div
22
+ data-testid="warning-alert"
23
+ className={`rounded-md border-l-4 p-4 ${borderColor} ${bgColor}`}
24
+ >
25
+ <div className="flex items-start">
26
+ <div className="flex-1">
27
+ <p className="text-sm font-medium text-gray-900">{warning.message}</p>
28
+ <p className="mt-1 text-xs text-gray-600">{warning.relatedId}</p>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,24 @@
1
+ import React from "react";
2
+ import { WarningAlert } from "./warning-alert";
3
+ import type { Warning } from "@/lib/dashboard-data";
4
+
5
+ type WarningAlertsProps = {
6
+ warnings: Warning[];
7
+ };
8
+
9
+ export function WarningAlerts({ warnings }: WarningAlertsProps) {
10
+ if (warnings.length === 0) {
11
+ return null;
12
+ }
13
+
14
+ return (
15
+ <div className="rounded-lg border border-gray-200 bg-white p-6">
16
+ <h2 className="mb-4 text-lg font-semibold text-gray-900">Warnings</h2>
17
+ <div className="space-y-3">
18
+ {warnings.map((warning, index) => (
19
+ <WarningAlert key={index} warning={warning} />
20
+ ))}
21
+ </div>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+
3
+ const SEVERITY_STYLES: Record<string, string> = {
4
+ critical: "bg-red-500 text-white",
5
+ high: "bg-orange-500 text-white",
6
+ medium: "bg-yellow-500 text-white",
7
+ low: "bg-gray-400 text-white",
8
+ };
9
+
10
+ const TYPE_STYLES: Record<string, { bg: string; label: string }> = {
11
+ bug: { bg: "bg-red-100 text-red-800", label: "Bug" },
12
+ improvement: { bg: "bg-blue-100 text-blue-800", label: "Improvement" },
13
+ "requirement-gap": { bg: "bg-amber-100 text-amber-800", label: "Requirement Gap" },
14
+ "spec-mismatch": { bg: "bg-purple-100 text-purple-800", label: "Spec Mismatch" },
15
+ security: { bg: "bg-red-100 text-red-800", label: "Security" },
16
+ };
17
+
18
+ export function FeedbackBadge({
19
+ type,
20
+ severity,
21
+ }: {
22
+ type?: string;
23
+ severity?: string;
24
+ }) {
25
+ const typeStyle = type ? TYPE_STYLES[type] : undefined;
26
+ const severityStyle = severity ? SEVERITY_STYLES[severity] : undefined;
27
+
28
+ return (
29
+ <span className="inline-flex items-center gap-1.5">
30
+ {typeStyle && (
31
+ <span
32
+ className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${typeStyle.bg}`}
33
+ data-testid={`feedback-badge-${type}`}
34
+ >
35
+ {typeStyle.label}
36
+ </span>
37
+ )}
38
+ {severityStyle && (
39
+ <span
40
+ className={`inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium ${severityStyle}`}
41
+ data-testid="feedback-severity"
42
+ >
43
+ {severity}
44
+ </span>
45
+ )}
46
+ </span>
47
+ );
48
+ }
@@ -0,0 +1,38 @@
1
+ "use client";
2
+
3
+ import React, { useState, useMemo } from "react";
4
+ import type { FeedbackEntry } from "@reqord/shared";
5
+ import { FeedbackFilters, type FeedbackFilterState } from "./feedback-filters";
6
+ import { FeedbackTable } from "./feedback-table";
7
+
8
+ export function FeedbackClientView({
9
+ feedbacks,
10
+ requirementTitles,
11
+ specificationTitles,
12
+ }: {
13
+ feedbacks: FeedbackEntry[];
14
+ requirementTitles: Record<string, string>;
15
+ specificationTitles: Record<string, string>;
16
+ }) {
17
+ const [filters, setFilters] = useState<FeedbackFilterState>({});
18
+
19
+ const filteredFeedbacks = useMemo(() => {
20
+ return feedbacks.filter((fb) => {
21
+ if (filters.type && fb.type !== filters.type) return false;
22
+ if (filters.severity && fb.severity !== filters.severity) return false;
23
+ if (filters.status && fb.status !== filters.status) return false;
24
+ return true;
25
+ });
26
+ }, [feedbacks, filters]);
27
+
28
+ return (
29
+ <div className="space-y-4" data-testid="feedback-client-view">
30
+ <FeedbackFilters activeFilters={filters} onFilterChange={setFilters} />
31
+ <FeedbackTable
32
+ feedbacks={filteredFeedbacks}
33
+ requirementTitles={requirementTitles}
34
+ specificationTitles={specificationTitles}
35
+ />
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ export interface FeedbackFilterState {
6
+ type?: string;
7
+ severity?: string;
8
+ status?: string;
9
+ }
10
+
11
+ const TYPE_OPTIONS = ["all", "bug", "improvement", "requirement-gap", "spec-mismatch", "security"];
12
+ const SEVERITY_OPTIONS = ["all", "critical", "high", "medium", "low"];
13
+ const STATUS_OPTIONS = ["all", "open", "closed"];
14
+
15
+ function SegmentedButtons({
16
+ label,
17
+ options,
18
+ value,
19
+ onChange,
20
+ }: {
21
+ label: string;
22
+ options: string[];
23
+ value: string;
24
+ onChange: (v: string) => void;
25
+ }) {
26
+ return (
27
+ <div className="flex items-center gap-2">
28
+ <span className="text-xs font-medium text-gray-500 w-16">{label}</span>
29
+ <div className="flex gap-1">
30
+ {options.map((opt) => (
31
+ <button
32
+ key={opt}
33
+ onClick={() => onChange(opt)}
34
+ aria-label={`${label} ${opt}`}
35
+ aria-pressed={value === opt}
36
+ className={`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
37
+ value === opt
38
+ ? "bg-blue-600 text-white"
39
+ : "bg-gray-100 text-gray-600 hover:bg-gray-200"
40
+ }`}
41
+ data-testid={`filter-${label.toLowerCase()}-${opt}`}
42
+ >
43
+ {opt}
44
+ </button>
45
+ ))}
46
+ </div>
47
+ </div>
48
+ );
49
+ }
50
+
51
+ export function FeedbackFilters({
52
+ activeFilters,
53
+ onFilterChange,
54
+ }: {
55
+ activeFilters: FeedbackFilterState;
56
+ onFilterChange: (filters: FeedbackFilterState) => void;
57
+ }) {
58
+ return (
59
+ <div className="flex flex-wrap gap-4 rounded-lg border border-gray-200 bg-white p-4" data-testid="feedback-filters">
60
+ <SegmentedButtons
61
+ label="Type"
62
+ options={TYPE_OPTIONS}
63
+ value={activeFilters.type ?? "all"}
64
+ onChange={(v) =>
65
+ onFilterChange({ ...activeFilters, type: v === "all" ? undefined : v })
66
+ }
67
+ />
68
+ <SegmentedButtons
69
+ label="Severity"
70
+ options={SEVERITY_OPTIONS}
71
+ value={activeFilters.severity ?? "all"}
72
+ onChange={(v) =>
73
+ onFilterChange({ ...activeFilters, severity: v === "all" ? undefined : v })
74
+ }
75
+ />
76
+ <SegmentedButtons
77
+ label="Status"
78
+ options={STATUS_OPTIONS}
79
+ value={activeFilters.status ?? "all"}
80
+ onChange={(v) =>
81
+ onFilterChange({ ...activeFilters, status: v === "all" ? undefined : v })
82
+ }
83
+ />
84
+ </div>
85
+ );
86
+ }
@@ -0,0 +1,93 @@
1
+ import React from "react";
2
+ import Link from "next/link";
3
+ import type { FeedbackLinkedTo } from "@reqord/shared";
4
+
5
+ export function FeedbackLinkedItems({
6
+ linkedTo,
7
+ requirementTitles,
8
+ specificationTitles,
9
+ }: {
10
+ linkedTo: FeedbackLinkedTo;
11
+ requirementTitles: Record<string, string>;
12
+ specificationTitles: Record<string, string>;
13
+ }) {
14
+ const hasAny =
15
+ linkedTo.requirements.length > 0 ||
16
+ linkedTo.createdRequirements.length > 0 ||
17
+ linkedTo.specifications.length > 0 ||
18
+ linkedTo.createdSpecifications.length > 0 ||
19
+ (linkedTo.resolved &&
20
+ (linkedTo.resolved.requirements.length > 0 ||
21
+ linkedTo.resolved.specifications.length > 0));
22
+
23
+ if (!hasAny) return null;
24
+
25
+ return (
26
+ <div className="flex flex-wrap gap-1" data-testid="feedback-linked-items">
27
+ {linkedTo.requirements.map((id) => (
28
+ <Link
29
+ key={`req-${id}`}
30
+ href={`/requirements/${id}`}
31
+ className="inline-flex items-center rounded-full bg-blue-50 px-2 py-0.5 text-xs text-blue-700 hover:bg-blue-100"
32
+ data-testid="linked-requirement"
33
+ >
34
+ {requirementTitles[id] ?? id}
35
+ </Link>
36
+ ))}
37
+ {linkedTo.createdRequirements.map((id) => (
38
+ <Link
39
+ key={`created-req-${id}`}
40
+ href={`/requirements/${id}`}
41
+ className="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-0.5 text-xs text-green-700 hover:bg-green-100"
42
+ data-testid="created-requirement"
43
+ >
44
+ {requirementTitles[id] ?? id}
45
+ <span className="rounded bg-green-200 px-1 text-green-800">created</span>
46
+ </Link>
47
+ ))}
48
+ {linkedTo.specifications.map((id) => (
49
+ <Link
50
+ key={`spec-${id}`}
51
+ href={`/specifications/${id}`}
52
+ className="inline-flex items-center rounded-full bg-purple-50 px-2 py-0.5 text-xs text-purple-700 hover:bg-purple-100"
53
+ data-testid="linked-specification"
54
+ >
55
+ {specificationTitles[id] ?? id}
56
+ </Link>
57
+ ))}
58
+ {linkedTo.createdSpecifications.map((id) => (
59
+ <Link
60
+ key={`created-spec-${id}`}
61
+ href={`/specifications/${id}`}
62
+ className="inline-flex items-center gap-1 rounded-full bg-emerald-50 px-2 py-0.5 text-xs text-emerald-700 hover:bg-emerald-100"
63
+ data-testid="created-specification"
64
+ >
65
+ {specificationTitles[id] ?? id}
66
+ <span className="rounded bg-emerald-200 px-1 text-emerald-800">created</span>
67
+ </Link>
68
+ ))}
69
+ {linkedTo.resolved?.requirements.map((id) => (
70
+ <Link
71
+ key={`resolved-req-${id}`}
72
+ href={`/requirements/${id}`}
73
+ className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600 hover:bg-gray-200"
74
+ data-testid="resolved-requirement"
75
+ >
76
+ {requirementTitles[id] ?? id}
77
+ <span className="rounded bg-gray-300 px-1 text-gray-700">resolved</span>
78
+ </Link>
79
+ ))}
80
+ {linkedTo.resolved?.specifications.map((id) => (
81
+ <Link
82
+ key={`resolved-spec-${id}`}
83
+ href={`/specifications/${id}`}
84
+ className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600 hover:bg-gray-200"
85
+ data-testid="resolved-specification"
86
+ >
87
+ {specificationTitles[id] ?? id}
88
+ <span className="rounded bg-gray-300 px-1 text-gray-700">resolved</span>
89
+ </Link>
90
+ ))}
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,40 @@
1
+ import React from "react";
2
+ import type { FeedbackEntry } from "@reqord/shared";
3
+ import { FeedbackBadge } from "./feedback-badge";
4
+
5
+ export function FeedbackList({ feedbacks }: { feedbacks: FeedbackEntry[] }) {
6
+ if (feedbacks.length === 0) return null;
7
+
8
+ return (
9
+ <div className="rounded-lg border border-gray-200 bg-white p-4" data-testid="feedback-list">
10
+ <h2 className="mb-3 text-sm font-semibold text-gray-700">
11
+ Unresolved Feedback ({feedbacks.length})
12
+ </h2>
13
+ <div className="space-y-3">
14
+ {feedbacks.map((feedback) => (
15
+ <div
16
+ key={feedback.githubIssue}
17
+ className="rounded-md border border-gray-100 bg-gray-50 p-3"
18
+ data-testid="feedback-item"
19
+ >
20
+ <div className="flex min-w-0 items-center gap-2">
21
+ <span className="shrink-0 font-mono text-sm text-blue-600" data-testid="feedback-issue">
22
+ #{feedback.githubIssue}
23
+ </span>
24
+ {feedback.title && (
25
+ <span className="truncate text-sm text-gray-700" data-testid="feedback-title">{feedback.title}</span>
26
+ )}
27
+ <FeedbackBadge
28
+ type={feedback.type}
29
+ severity={feedback.severity}
30
+ />
31
+ </div>
32
+ <p className="mt-1 text-xs text-gray-400">
33
+ {new Date(feedback.syncedAt).toLocaleDateString("ja-JP")}
34
+ </p>
35
+ </div>
36
+ ))}
37
+ </div>
38
+ </div>
39
+ );
40
+ }