@reqord/web 0.1.0 → 0.3.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 (46) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/components/dashboard/critical-path-display.test.tsx +61 -0
  3. package/src/__tests__/components/dashboard/progress-bar.test.tsx +63 -0
  4. package/src/__tests__/components/dashboard/project-health.test.tsx +21 -7
  5. package/src/__tests__/components/dashboard/status-card.test.tsx +86 -0
  6. package/src/__tests__/components/dashboard/warning-alert.test.tsx +6 -6
  7. package/src/__tests__/components/feedback/feedback-filters-improved.test.tsx +33 -0
  8. package/src/__tests__/components/graph/drilldown-breadcrumb.test.tsx +12 -0
  9. package/src/__tests__/components/graph/edge-styles.test.ts +6 -6
  10. package/src/__tests__/components/graph/issue-node.test.tsx +25 -6
  11. package/src/__tests__/components/graph/requirement-node.test.tsx +45 -0
  12. package/src/__tests__/components/graph/specification-node.test.tsx +27 -14
  13. package/src/__tests__/components/requirement/requirement-table.test.tsx +165 -0
  14. package/src/__tests__/components/specification/specification-table.test.tsx +189 -0
  15. package/src/__tests__/components/ui/badge.test.tsx +98 -0
  16. package/src/__tests__/components/ui/button.test.tsx +98 -0
  17. package/src/__tests__/components/ui/card.test.tsx +58 -0
  18. package/src/__tests__/components/ui/nav.test.tsx +91 -0
  19. package/src/__tests__/components/ui/tabs.test.tsx +53 -0
  20. package/src/__tests__/lib/drilldown-graph-data.test.ts +45 -3
  21. package/src/app/dashboard/page.tsx +29 -21
  22. package/src/app/globals.css +46 -0
  23. package/src/app/layout.tsx +4 -1
  24. package/src/app/requirements/loading.tsx +30 -5
  25. package/src/app/specifications/loading.tsx +29 -5
  26. package/src/components/dashboard/critical-path-display.tsx +30 -15
  27. package/src/components/dashboard/progress-bar.tsx +2 -4
  28. package/src/components/dashboard/project-health.tsx +9 -10
  29. package/src/components/dashboard/status-card.tsx +20 -9
  30. package/src/components/dashboard/warning-alert.tsx +57 -5
  31. package/src/components/feedback/feedback-filters.tsx +41 -12
  32. package/src/components/graph/drilldown-breadcrumb.tsx +1 -1
  33. package/src/components/graph/drilldown-graph.tsx +3 -1
  34. package/src/components/graph/edge-styles.ts +3 -3
  35. package/src/components/graph/issue-node.tsx +7 -7
  36. package/src/components/graph/multi-level-graph.tsx +2 -2
  37. package/src/components/graph/requirement-node.tsx +5 -5
  38. package/src/components/graph/specification-node.tsx +12 -9
  39. package/src/components/requirement/requirement-table.tsx +62 -18
  40. package/src/components/specification/specification-table.tsx +59 -17
  41. package/src/components/ui/badge.tsx +4 -4
  42. package/src/components/ui/button.tsx +43 -0
  43. package/src/components/ui/card.tsx +25 -0
  44. package/src/components/ui/nav.tsx +35 -35
  45. package/src/components/ui/tabs.tsx +2 -0
  46. package/src/lib/drilldown-graph-data.ts +23 -4
@@ -5,24 +5,76 @@ type WarningAlertProps = {
5
5
  warning: Warning;
6
6
  };
7
7
 
8
+ function ErrorIcon() {
9
+ return (
10
+ <svg
11
+ className="h-5 w-5 text-red-500"
12
+ viewBox="0 0 20 20"
13
+ fill="currentColor"
14
+ aria-hidden="true"
15
+ >
16
+ <path
17
+ fillRule="evenodd"
18
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
19
+ clipRule="evenodd"
20
+ />
21
+ </svg>
22
+ );
23
+ }
24
+
25
+ function WarningIcon() {
26
+ return (
27
+ <svg
28
+ className="h-5 w-5 text-yellow-500"
29
+ viewBox="0 0 20 20"
30
+ fill="currentColor"
31
+ aria-hidden="true"
32
+ >
33
+ <path
34
+ fillRule="evenodd"
35
+ d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
36
+ clipRule="evenodd"
37
+ />
38
+ </svg>
39
+ );
40
+ }
41
+
42
+ function InfoIcon() {
43
+ return (
44
+ <svg
45
+ className="h-5 w-5 text-blue-500"
46
+ viewBox="0 0 20 20"
47
+ fill="currentColor"
48
+ aria-hidden="true"
49
+ >
50
+ <path
51
+ fillRule="evenodd"
52
+ d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
53
+ clipRule="evenodd"
54
+ />
55
+ </svg>
56
+ );
57
+ }
58
+
8
59
  export function WarningAlert({ warning }: WarningAlertProps) {
9
- let borderColor = "border-blue-500";
10
60
  let bgColor = "bg-blue-50";
61
+ let icon = <InfoIcon />;
11
62
 
12
63
  if (warning.severity === "error") {
13
- borderColor = "border-red-500";
14
64
  bgColor = "bg-red-50";
65
+ icon = <ErrorIcon />;
15
66
  } else if (warning.severity === "warning") {
16
- borderColor = "border-yellow-500";
17
67
  bgColor = "bg-yellow-50";
68
+ icon = <WarningIcon />;
18
69
  }
19
70
 
20
71
  return (
21
72
  <div
22
73
  data-testid="warning-alert"
23
- className={`rounded-md border-l-4 p-4 ${borderColor} ${bgColor}`}
74
+ className={`rounded-lg p-4 ${bgColor}`}
24
75
  >
25
- <div className="flex items-start">
76
+ <div className="flex items-start gap-3">
77
+ <div className="flex-shrink-0 pt-0.5">{icon}</div>
26
78
  <div className="flex-1">
27
79
  <p className="text-sm font-medium text-gray-900">{warning.message}</p>
28
80
  <p className="mt-1 text-xs text-gray-600">{warning.relatedId}</p>
@@ -8,7 +8,15 @@ export interface FeedbackFilterState {
8
8
  status?: string;
9
9
  }
10
10
 
11
- const TYPE_OPTIONS = ["all", "bug", "improvement", "requirement-gap", "spec-mismatch", "security"];
11
+ const TYPE_OPTIONS: { value: string; label: string }[] = [
12
+ { value: "all", label: "all" },
13
+ { value: "bug", label: "bug" },
14
+ { value: "improvement", label: "improvement" },
15
+ { value: "requirement-gap", label: "Req Gap" },
16
+ { value: "spec-mismatch", label: "Spec Mismatch" },
17
+ { value: "security", label: "security" },
18
+ ];
19
+
12
20
  const SEVERITY_OPTIONS = ["all", "critical", "high", "medium", "low"];
13
21
  const STATUS_OPTIONS = ["all", "open", "closed"];
14
22
 
@@ -19,7 +27,7 @@ function SegmentedButtons({
19
27
  onChange,
20
28
  }: {
21
29
  label: string;
22
- options: string[];
30
+ options: { value: string; label: string }[];
23
31
  value: string;
24
32
  onChange: (v: string) => void;
25
33
  }) {
@@ -29,18 +37,18 @@ function SegmentedButtons({
29
37
  <div className="flex gap-1">
30
38
  {options.map((opt) => (
31
39
  <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
40
+ key={opt.value}
41
+ onClick={() => onChange(opt.value)}
42
+ aria-label={`${label} ${opt.label}`}
43
+ aria-pressed={value === opt.value}
44
+ className={`rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
45
+ value === opt.value
38
46
  ? "bg-blue-600 text-white"
39
47
  : "bg-gray-100 text-gray-600 hover:bg-gray-200"
40
48
  }`}
41
- data-testid={`filter-${label.toLowerCase()}-${opt}`}
49
+ data-testid={`filter-${label.toLowerCase()}-${opt.value}`}
42
50
  >
43
- {opt}
51
+ {opt.label}
44
52
  </button>
45
53
  ))}
46
54
  </div>
@@ -48,6 +56,27 @@ function SegmentedButtons({
48
56
  );
49
57
  }
50
58
 
59
+ function SimpleSegmentedButtons({
60
+ label,
61
+ options,
62
+ value,
63
+ onChange,
64
+ }: {
65
+ label: string;
66
+ options: string[];
67
+ value: string;
68
+ onChange: (v: string) => void;
69
+ }) {
70
+ return (
71
+ <SegmentedButtons
72
+ label={label}
73
+ options={options.map((o) => ({ value: o, label: o }))}
74
+ value={value}
75
+ onChange={onChange}
76
+ />
77
+ );
78
+ }
79
+
51
80
  export function FeedbackFilters({
52
81
  activeFilters,
53
82
  onFilterChange,
@@ -65,7 +94,7 @@ export function FeedbackFilters({
65
94
  onFilterChange({ ...activeFilters, type: v === "all" ? undefined : v })
66
95
  }
67
96
  />
68
- <SegmentedButtons
97
+ <SimpleSegmentedButtons
69
98
  label="Severity"
70
99
  options={SEVERITY_OPTIONS}
71
100
  value={activeFilters.severity ?? "all"}
@@ -73,7 +102,7 @@ export function FeedbackFilters({
73
102
  onFilterChange({ ...activeFilters, severity: v === "all" ? undefined : v })
74
103
  }
75
104
  />
76
- <SegmentedButtons
105
+ <SimpleSegmentedButtons
77
106
  label="Status"
78
107
  options={STATUS_OPTIONS}
79
108
  value={activeFilters.status ?? "all"}
@@ -23,7 +23,7 @@ export function DrillDownBreadcrumb({ requirementTitle, onBack }: DrillDownBread
23
23
  <div className="mb-4 flex items-center gap-2 text-sm text-gray-600">
24
24
  <button
25
25
  onClick={onBack}
26
- className="flex items-center gap-1 rounded px-3 py-1 hover:bg-gray-100"
26
+ className="flex items-center gap-1 rounded px-3 py-1 hover:bg-warm-200/60 hover:text-warm-900"
27
27
  >
28
28
  ← Back to overview
29
29
  </button>
@@ -2,6 +2,7 @@
2
2
 
3
3
  import React, { useMemo } from "react";
4
4
  import { ReactFlow, Background, Controls } from "@xyflow/react";
5
+ import "@xyflow/react/dist/style.css";
5
6
  import { RequirementNode } from "./requirement-node";
6
7
  import { SpecificationNode } from "./specification-node";
7
8
  import { IssueNode } from "./issue-node";
@@ -33,7 +34,8 @@ export function DrillDownGraph({ requirement, specifications, tasks }: DrillDown
33
34
  edges={edges}
34
35
  nodeTypes={nodeTypes}
35
36
  fitView
36
- minZoom={0.1}
37
+ fitViewOptions={{ padding: 0.1, maxZoom: 1 }}
38
+ minZoom={0.2}
37
39
  maxZoom={2}
38
40
  proOptions={{ hideAttribution: true }}
39
41
  >
@@ -1,15 +1,15 @@
1
1
  export const EDGE_STYLES = {
2
2
  dependency: {
3
- stroke: "#64748b",
3
+ stroke: "#94a3b8",
4
4
  strokeWidth: 2,
5
5
  },
6
6
  implements: {
7
- stroke: "#3b82f6",
7
+ stroke: "#dc2626",
8
8
  strokeWidth: 2,
9
9
  strokeDasharray: "5,5",
10
10
  },
11
11
  tracks: {
12
- stroke: "#22c55e",
12
+ stroke: "#a855f7",
13
13
  strokeWidth: 1.5,
14
14
  strokeDasharray: "2,2",
15
15
  },
@@ -4,9 +4,9 @@ import React, { memo } from "react";
4
4
  import { Handle, Position, type NodeProps } from "@xyflow/react";
5
5
 
6
6
  const STATUS_COLORS: Record<string, string> = {
7
- open: "bg-yellow-200",
8
- in_progress: "bg-blue-200",
9
- closed: "bg-green-200",
7
+ open: "border border-amber-300 bg-amber-50",
8
+ in_progress: "border border-blue-300 bg-blue-50",
9
+ closed: "border border-emerald-300 bg-emerald-50",
10
10
  };
11
11
 
12
12
  type IssueNodeData = {
@@ -18,7 +18,7 @@ type IssueNodeData = {
18
18
 
19
19
  function IssueNodeComponent({ data }: NodeProps) {
20
20
  const nodeData = data as IssueNodeData;
21
- const bgClass = STATUS_COLORS[nodeData.status] ?? "bg-gray-200";
21
+ const colorClass = STATUS_COLORS[nodeData.status] ?? "border border-gray-300 bg-gray-50";
22
22
 
23
23
  const handleClick = () => {
24
24
  if (nodeData.issueUrl) {
@@ -28,14 +28,14 @@ function IssueNodeComponent({ data }: NodeProps) {
28
28
 
29
29
  return (
30
30
  <div
31
- className={`rounded-lg border shadow-sm px-3 py-2 cursor-pointer ${bgClass}`}
31
+ className={`rounded-lg shadow-sm px-3 py-2 cursor-pointer ${colorClass}`}
32
32
  style={{ width: 180 }}
33
33
  onClick={handleClick}
34
34
  >
35
- <Handle type="target" position={Position.Left} className="!bg-gray-400" />
35
+ <Handle type="target" position={Position.Left} className="!bg-gray-300" />
36
36
  <p className="truncate text-sm font-medium text-gray-900">{nodeData.label}</p>
37
37
  <div className="mt-1">
38
- <span className="inline-block rounded-full bg-white px-2 py-0.5 text-xs text-gray-700">
38
+ <span className="inline-block rounded-full bg-white/80 backdrop-blur-sm px-2 py-0.5 text-xs text-gray-700">
39
39
  {nodeData.status}
40
40
  </span>
41
41
  </div>
@@ -30,8 +30,8 @@ const EDGE_STYLES = {
30
30
  animated: false,
31
31
  },
32
32
  implements: {
33
- style: { stroke: "#6366f1", strokeWidth: 2, strokeDasharray: "5,5" },
34
- markerEnd: { type: "arrowclosed" as const, color: "#6366f1" },
33
+ style: { stroke: "#dc2626", strokeWidth: 2, strokeDasharray: "5,5" },
34
+ markerEnd: { type: "arrowclosed" as const, color: "#dc2626" },
35
35
  animated: false,
36
36
  },
37
37
  tracks: {
@@ -6,8 +6,8 @@ import { useRouter } from "next/navigation";
6
6
 
7
7
  const STATUS_COLORS: Record<string, string> = {
8
8
  draft: "border-gray-300 bg-gray-50",
9
- approved: "border-green-300 bg-green-50",
10
- implemented: "border-blue-300 bg-blue-50",
9
+ approved: "border-blue-300 bg-blue-50",
10
+ implemented: "border-emerald-300 bg-emerald-50",
11
11
  deprecated: "border-red-300 bg-red-50",
12
12
  };
13
13
 
@@ -43,11 +43,11 @@ function RequirementNodeComponent({ data, id }: NodeProps) {
43
43
 
44
44
  return (
45
45
  <div
46
- className={`cursor-pointer rounded-lg border-2 px-3 py-2 shadow-sm ${borderClass}`}
46
+ className={`cursor-pointer rounded-lg border-2 px-3 py-2 shadow-md ${borderClass}`}
47
47
  style={{ width: 220 }}
48
48
  onClick={handleBodyClick}
49
49
  >
50
- <Handle type="target" position={Position.Left} className="!bg-gray-400" />
50
+ <Handle type="target" position={Position.Left} className="!bg-gray-300" />
51
51
  <p className="truncate text-xs font-mono text-gray-500">{id}</p>
52
52
  <p className="mt-0.5 truncate text-sm font-medium text-gray-900">
53
53
  {nodeData.label}
@@ -61,7 +61,7 @@ function RequirementNodeComponent({ data, id }: NodeProps) {
61
61
  📄 {nodeData.specCount} spec{nodeData.specCount > 1 ? "s" : ""}
62
62
  </button>
63
63
  ) : null}
64
- <Handle type="source" position={Position.Right} className="!bg-gray-400" />
64
+ <Handle type="source" position={Position.Right} className="!bg-gray-300" />
65
65
  </div>
66
66
  );
67
67
  }
@@ -4,10 +4,10 @@ import React, { memo } from "react";
4
4
  import { Handle, Position, type NodeProps } from "@xyflow/react";
5
5
 
6
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",
7
+ draft: "border border-gray-300 bg-gray-100",
8
+ approved: "border border-blue-300 bg-blue-100",
9
+ implemented: "border border-emerald-300 bg-emerald-100",
10
+ deprecated: "border border-red-300 bg-red-100",
11
11
  };
12
12
 
13
13
  type SpecificationNodeData = {
@@ -17,21 +17,24 @@ type SpecificationNodeData = {
17
17
 
18
18
  function SpecificationNodeComponent({ data, id }: NodeProps) {
19
19
  const nodeData = data as SpecificationNodeData;
20
- const bgClass = STATUS_COLORS[nodeData.status] ?? "bg-gray-200";
20
+ const colorClass = STATUS_COLORS[nodeData.status] ?? "border border-gray-300 bg-gray-100";
21
21
 
22
22
  return (
23
23
  <div
24
- className={`rounded-lg border shadow-sm px-3 py-2 ${bgClass}`}
24
+ className={`rounded-lg shadow-sm px-3 py-2 ${colorClass}`}
25
25
  style={{ width: 200 }}
26
26
  >
27
- <Handle type="target" position={Position.Left} className="!bg-gray-400" />
27
+ <Handle type="target" position={Position.Left} className="!bg-gray-300" />
28
28
  <p className="truncate text-xs font-mono text-gray-900">{id}</p>
29
+ {nodeData.label && nodeData.label !== id && (
30
+ <p className="mt-0.5 truncate text-sm font-medium text-gray-800">{nodeData.label}</p>
31
+ )}
29
32
  <div className="mt-1">
30
- <span className="inline-block rounded-full bg-white px-2 py-0.5 text-xs text-gray-700">
33
+ <span className="inline-block rounded-full bg-white/80 backdrop-blur-sm px-2 py-0.5 text-xs text-gray-700">
31
34
  {nodeData.status}
32
35
  </span>
33
36
  </div>
34
- <Handle type="source" position={Position.Right} className="!bg-gray-400" />
37
+ <Handle type="source" position={Position.Right} className="!bg-gray-300" />
35
38
  </div>
36
39
  );
37
40
  }
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useMemo, useState } from "react";
3
+ import React, { useMemo, useState } from "react";
4
4
  import Link from "next/link";
5
5
  import type { Requirement, Status, Priority } from "@reqord/shared";
6
6
  import { StatusBadge, PriorityBadge, ComplexityBadge } from "@/components/ui/badge";
@@ -87,18 +87,38 @@ export function RequirementTable({ requirements }: { requirements: Requirement[]
87
87
  }
88
88
 
89
89
  function renderSortHeader(column: SortKey, label: string) {
90
- const arrow = sortKey === column ? (sortDir === "asc" ? " ↑" : " ↓") : "";
90
+ const isActive = sortKey === column;
91
91
  return (
92
92
  <th
93
93
  key={column}
94
- className="cursor-pointer px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 hover:text-gray-900"
95
- onClick={() => handleSort(column)}
94
+ scope="col"
95
+ aria-sort={isActive ? (sortDir === "asc" ? "ascending" : "descending") : undefined}
96
+ className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
96
97
  >
97
- {label}{arrow}
98
+ <button
99
+ type="button"
100
+ onClick={() => handleSort(column)}
101
+ className={`inline-flex items-center gap-1 transition-colors ${
102
+ isActive ? "text-warm-900 font-semibold" : "hover:text-gray-900"
103
+ }`}
104
+ >
105
+ {label}
106
+ <span aria-hidden="true" className={`text-[10px] ${isActive ? "opacity-100" : "opacity-30"}`}>
107
+ {isActive && sortDir === "desc" ? "▼" : "▲"}
108
+ </span>
109
+ </button>
98
110
  </th>
99
111
  );
100
112
  }
101
113
 
114
+ const hasActiveFilters = search || statusFilter !== "all" || priorityFilter !== "all";
115
+
116
+ function clearAllFilters() {
117
+ setSearch("");
118
+ setStatusFilter("all");
119
+ setPriorityFilter("all");
120
+ }
121
+
102
122
  return (
103
123
  <div className="space-y-4">
104
124
  <div className="flex flex-wrap items-center gap-3">
@@ -107,12 +127,14 @@ export function RequirementTable({ requirements }: { requirements: Requirement[]
107
127
  placeholder="Search by ID or title..."
108
128
  value={search}
109
129
  onChange={(e) => setSearch(e.target.value)}
130
+ aria-label="Search requirements by ID or title"
110
131
  className="rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
111
132
  />
112
133
  <select
113
134
  value={statusFilter}
114
135
  onChange={(e) => setStatusFilter(e.target.value as Status | "all")}
115
- className="rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm"
136
+ aria-label="Filter by status"
137
+ className="rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
116
138
  >
117
139
  {STATUS_OPTIONS.map((o) => (
118
140
  <option key={o.value} value={o.value}>
@@ -123,7 +145,8 @@ export function RequirementTable({ requirements }: { requirements: Requirement[]
123
145
  <select
124
146
  value={priorityFilter}
125
147
  onChange={(e) => setPriorityFilter(e.target.value as Priority | "all")}
126
- className="rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm"
148
+ aria-label="Filter by priority"
149
+ className="rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
127
150
  >
128
151
  {PRIORITY_OPTIONS.map((o) => (
129
152
  <option key={o.value} value={o.value}>
@@ -138,7 +161,7 @@ export function RequirementTable({ requirements }: { requirements: Requirement[]
138
161
 
139
162
  <div className="overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm">
140
163
  <table className="min-w-full divide-y divide-gray-200">
141
- <thead className="bg-gray-50">
164
+ <thead className="bg-gray-50 border-b-2 border-gray-200">
142
165
  <tr>
143
166
  {renderSortHeader("id", "ID")}
144
167
  {renderSortHeader("title", "Title")}
@@ -153,14 +176,35 @@ export function RequirementTable({ requirements }: { requirements: Requirement[]
153
176
  <tbody className="divide-y divide-gray-200">
154
177
  {filtered.length === 0 ? (
155
178
  <tr>
156
- <td colSpan={6} className="px-4 py-8 text-center text-sm text-gray-500">
157
- No requirements found.
179
+ <td colSpan={6} className="px-4 py-12 text-center">
180
+ <div className="text-gray-400">
181
+ <svg className="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
182
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
183
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
184
+ </svg>
185
+ <p className="mt-2 text-sm font-medium text-gray-900">No requirements found</p>
186
+ <p className="mt-1 text-sm text-gray-500">Try adjusting your search or filter criteria</p>
187
+ {hasActiveFilters && (
188
+ <button
189
+ type="button"
190
+ onClick={clearAllFilters}
191
+ className="mt-3 text-sm font-medium text-blue-600 hover:text-blue-800"
192
+ >
193
+ Clear all filters
194
+ </button>
195
+ )}
196
+ </div>
158
197
  </td>
159
198
  </tr>
160
199
  ) : (
161
- filtered.map((req) => (
162
- <tr key={req.id} className="hover:bg-gray-50">
163
- <td className="whitespace-nowrap px-4 py-3 text-sm font-mono">
200
+ filtered.map((req, index) => (
201
+ <tr
202
+ key={req.id}
203
+ className={`hover:bg-blue-50/50 transition-colors duration-150 ${
204
+ index % 2 === 1 ? "bg-gray-50/50" : ""
205
+ }`}
206
+ >
207
+ <td className="whitespace-nowrap px-4 py-3.5 text-sm font-mono">
164
208
  <Link
165
209
  href={`/requirements/${req.id}`}
166
210
  className="text-blue-600 hover:text-blue-800 hover:underline"
@@ -168,7 +212,7 @@ export function RequirementTable({ requirements }: { requirements: Requirement[]
168
212
  {req.id}
169
213
  </Link>
170
214
  </td>
171
- <td className="px-4 py-3 text-sm">
215
+ <td className="px-4 py-3.5 text-sm">
172
216
  <Link
173
217
  href={`/requirements/${req.id}`}
174
218
  className="text-gray-900 hover:text-blue-600"
@@ -176,20 +220,20 @@ export function RequirementTable({ requirements }: { requirements: Requirement[]
176
220
  {req.title}
177
221
  </Link>
178
222
  </td>
179
- <td className="whitespace-nowrap px-4 py-3">
223
+ <td className="whitespace-nowrap px-4 py-3.5">
180
224
  <StatusBadge status={req.status} />
181
225
  </td>
182
- <td className="whitespace-nowrap px-4 py-3">
226
+ <td className="whitespace-nowrap px-4 py-3.5">
183
227
  <PriorityBadge priority={req.priority} />
184
228
  </td>
185
- <td className="whitespace-nowrap px-4 py-3">
229
+ <td className="whitespace-nowrap px-4 py-3.5">
186
230
  {req.estimatedComplexity ? (
187
231
  <ComplexityBadge complexity={req.estimatedComplexity} />
188
232
  ) : (
189
233
  <span className="text-xs text-gray-400">-</span>
190
234
  )}
191
235
  </td>
192
- <td className="whitespace-nowrap px-4 py-3 text-sm text-gray-500">
236
+ <td className="whitespace-nowrap px-4 py-3.5 text-sm text-gray-500">
193
237
  {new Date(req.updatedAt).toLocaleDateString("ja-JP")}
194
238
  </td>
195
239
  </tr>