@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,228 @@
1
+ import Link from "next/link";
2
+ import type { Requirement, Specification, FeedbackEntry } from "@reqord/shared";
3
+ import { StatusBadge, PriorityBadge, ComplexityBadge } from "@/components/ui/badge";
4
+ import { FeedbackList } from "@/components/feedback/feedback-list";
5
+ import { DeleteButton } from "./delete-button";
6
+ import { RequirementTabs } from "./requirement-tabs";
7
+
8
+ export function RequirementDetail({
9
+ requirement,
10
+ description,
11
+ specifications = [],
12
+ feedbacks,
13
+ }: {
14
+ requirement: Requirement;
15
+ description: string | null;
16
+ specifications?: Specification[];
17
+ feedbacks?: FeedbackEntry[];
18
+ }) {
19
+ const { dependencies } = requirement;
20
+ const hasDeps =
21
+ dependencies.blockedBy.length > 0 ||
22
+ dependencies.blocks.length > 0 ||
23
+ dependencies.relatedTo.length > 0;
24
+
25
+ return (
26
+ <div className="space-y-6">
27
+ {/* Header */}
28
+ <div className="flex items-start justify-between">
29
+ <div>
30
+ <p className="text-sm text-gray-500">
31
+ <Link href="/requirements" className="hover:text-blue-600">
32
+ Requirements
33
+ </Link>
34
+ {" / "}
35
+ <span className="font-mono">{requirement.id}</span>
36
+ </p>
37
+ <h1 className="mt-1 text-2xl font-bold">{requirement.title}</h1>
38
+ </div>
39
+ <div className="flex items-center gap-2">
40
+ <Link
41
+ href={`/requirements/${requirement.id}/edit`}
42
+ className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
43
+ >
44
+ Edit
45
+ </Link>
46
+ <DeleteButton id={requirement.id} />
47
+ </div>
48
+ </div>
49
+
50
+ {/* Badges */}
51
+ <div className="flex flex-wrap gap-2">
52
+ <StatusBadge status={requirement.status} />
53
+ <PriorityBadge priority={requirement.priority} />
54
+ {requirement.estimatedComplexity ? (
55
+ <ComplexityBadge complexity={requirement.estimatedComplexity} />
56
+ ) : null}
57
+ {requirement.estimatedHours ? (
58
+ <span className="inline-flex items-center rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700">
59
+ {requirement.estimatedHours}h
60
+ </span>
61
+ ) : null}
62
+ </div>
63
+
64
+ {/* Meta */}
65
+ <div className="grid grid-cols-2 gap-4 rounded-lg border border-gray-200 bg-white p-4 text-sm sm:grid-cols-4">
66
+ <div>
67
+ <p className="text-gray-500">Version</p>
68
+ <p className="font-medium">{requirement.version}</p>
69
+ </div>
70
+ <div>
71
+ <p className="text-gray-500">Format</p>
72
+ <p className="font-medium">{requirement.format.type}</p>
73
+ </div>
74
+ <div>
75
+ <p className="text-gray-500">Created</p>
76
+ <p className="font-medium">
77
+ {new Date(requirement.createdAt).toLocaleDateString("ja-JP")}
78
+ </p>
79
+ </div>
80
+ <div>
81
+ <p className="text-gray-500">Updated</p>
82
+ <p className="font-medium">
83
+ {new Date(requirement.updatedAt).toLocaleDateString("ja-JP")}
84
+ </p>
85
+ </div>
86
+ </div>
87
+
88
+ {/* Format Details */}
89
+ {requirement.format.type === "user-story" ? (
90
+ <div className="rounded-lg border border-gray-200 bg-white p-4">
91
+ <h2 className="mb-2 text-sm font-semibold text-gray-700">User Story</h2>
92
+ <div className="space-y-1 text-sm">
93
+ <p>
94
+ <span className="font-medium text-gray-500">As a</span>{" "}
95
+ {requirement.format.userStory.as}
96
+ </p>
97
+ <p>
98
+ <span className="font-medium text-gray-500">I want</span>{" "}
99
+ {requirement.format.userStory.iWant}
100
+ </p>
101
+ <p>
102
+ <span className="font-medium text-gray-500">So that</span>{" "}
103
+ {requirement.format.userStory.soThat}
104
+ </p>
105
+ </div>
106
+ </div>
107
+ ) : null}
108
+
109
+ {requirement.format.type === "ears" ? (
110
+ <div className="rounded-lg border border-gray-200 bg-white p-4">
111
+ <h2 className="mb-2 text-sm font-semibold text-gray-700">EARS Format</h2>
112
+ <div className="space-y-1 text-sm">
113
+ <p>
114
+ <span className="font-medium text-gray-500">Type:</span>{" "}
115
+ {requirement.format.ears.type}
116
+ </p>
117
+ {requirement.format.ears.trigger ? (
118
+ <p>
119
+ <span className="font-medium text-gray-500">Trigger:</span>{" "}
120
+ {requirement.format.ears.trigger}
121
+ </p>
122
+ ) : null}
123
+ {requirement.format.ears.condition ? (
124
+ <p>
125
+ <span className="font-medium text-gray-500">Condition:</span>{" "}
126
+ {requirement.format.ears.condition}
127
+ </p>
128
+ ) : null}
129
+ <p>
130
+ <span className="font-medium text-gray-500">Action:</span>{" "}
131
+ {requirement.format.ears.action}
132
+ </p>
133
+ {requirement.format.ears.response ? (
134
+ <p>
135
+ <span className="font-medium text-gray-500">Response:</span>{" "}
136
+ {requirement.format.ears.response}
137
+ </p>
138
+ ) : null}
139
+ </div>
140
+ </div>
141
+ ) : null}
142
+
143
+ {/* Feedbacks */}
144
+ {feedbacks && feedbacks.length > 0 ? (
145
+ <FeedbackList feedbacks={feedbacks} />
146
+ ) : null}
147
+
148
+ {/* Dependencies */}
149
+ {hasDeps ? (
150
+ <div className="rounded-lg border border-gray-200 bg-white p-4">
151
+ <h2 className="mb-2 text-sm font-semibold text-gray-700">Dependencies</h2>
152
+ <div className="space-y-2 text-sm">
153
+ {dependencies.blockedBy.length > 0 ? (
154
+ <div>
155
+ <span className="font-medium text-gray-500">Blocked by: </span>
156
+ {dependencies.blockedBy.map((id) => (
157
+ <Link
158
+ key={id}
159
+ href={`/requirements/${id}`}
160
+ className="mr-2 font-mono text-blue-600 hover:underline"
161
+ >
162
+ {id}
163
+ </Link>
164
+ ))}
165
+ </div>
166
+ ) : null}
167
+ {dependencies.blocks.length > 0 ? (
168
+ <div>
169
+ <span className="font-medium text-gray-500">Blocks: </span>
170
+ {dependencies.blocks.map((id) => (
171
+ <Link
172
+ key={id}
173
+ href={`/requirements/${id}`}
174
+ className="mr-2 font-mono text-blue-600 hover:underline"
175
+ >
176
+ {id}
177
+ </Link>
178
+ ))}
179
+ </div>
180
+ ) : null}
181
+ {dependencies.relatedTo.length > 0 ? (
182
+ <div>
183
+ <span className="font-medium text-gray-500">Related to: </span>
184
+ {dependencies.relatedTo.map((id) => (
185
+ <Link
186
+ key={id}
187
+ href={`/requirements/${id}`}
188
+ className="mr-2 font-mono text-blue-600 hover:underline"
189
+ >
190
+ {id}
191
+ </Link>
192
+ ))}
193
+ </div>
194
+ ) : null}
195
+ </div>
196
+ </div>
197
+ ) : null}
198
+
199
+ {/* Specifications */}
200
+ <div className="rounded-lg border border-gray-200 bg-white p-4">
201
+ <h2 className="mb-2 text-sm font-semibold text-gray-700">Specifications</h2>
202
+ {specifications.length > 0 ? (
203
+ <div className="space-y-1 text-sm">
204
+ {specifications.map((spec) => (
205
+ <div key={spec.id} className="flex items-center gap-2">
206
+ <Link
207
+ href={`/specifications/${spec.id}`}
208
+ className="font-mono text-blue-600 hover:underline"
209
+ >
210
+ {spec.id}
211
+ </Link>
212
+ <StatusBadge status={spec.status} />
213
+ </div>
214
+ ))}
215
+ </div>
216
+ ) : (
217
+ <p className="text-sm text-gray-500">No specifications</p>
218
+ )}
219
+ </div>
220
+
221
+ {/* Success Criteria & Description Tabs */}
222
+ <RequirementTabs
223
+ successCriteria={requirement.successCriteria}
224
+ description={description}
225
+ />
226
+ </div>
227
+ );
228
+ }
@@ -0,0 +1,390 @@
1
+ "use client";
2
+
3
+ import { useActionState, useState } from "react";
4
+ import Link from "next/link";
5
+ import type { Requirement } from "@reqord/shared";
6
+ import { createRequirement, updateRequirement, type ActionResult } from "@/lib/actions";
7
+ import { SuccessCriteriaEditor } from "./success-criteria-editor";
8
+ import { DependencyEditor } from "./dependency-editor";
9
+ import { MarkdownEditor } from "./markdown-editor";
10
+
11
+ type Mode = "create" | "edit";
12
+
13
+ interface Props {
14
+ mode: Mode;
15
+ requirement?: Requirement;
16
+ description?: string | null;
17
+ allRequirements: Requirement[];
18
+ }
19
+
20
+ const INITIAL_STATE: ActionResult = { success: true };
21
+
22
+ export function RequirementForm({ mode, requirement, description, allRequirements }: Props) {
23
+ const action = mode === "create" ? createRequirement : updateRequirement;
24
+ const [state, formAction, isPending] = useActionState(
25
+ async (_prev: ActionResult, formData: FormData) => action(formData),
26
+ INITIAL_STATE,
27
+ );
28
+
29
+ const [title, setTitle] = useState(requirement?.title ?? "");
30
+ const [status, setStatus] = useState<string>(requirement?.status ?? "draft");
31
+ const [priority, setPriority] = useState<string>(requirement?.priority ?? "medium");
32
+ const [formatType, setFormatType] = useState<string>(requirement?.format.type ?? "free-form");
33
+ const [estimatedComplexity, setEstimatedComplexity] = useState(
34
+ requirement?.estimatedComplexity ?? "",
35
+ );
36
+ const [estimatedHours, setEstimatedHours] = useState(
37
+ requirement?.estimatedHours?.toString() ?? "",
38
+ );
39
+ const [successCriteria, setSuccessCriteria] = useState<string[]>(
40
+ requirement?.successCriteria ?? [],
41
+ );
42
+ const [blockedBy, setBlockedBy] = useState<string[]>(
43
+ requirement?.dependencies.blockedBy ?? [],
44
+ );
45
+ const [blocks, setBlocks] = useState<string[]>(
46
+ requirement?.dependencies.blocks ?? [],
47
+ );
48
+ const [relatedTo, setRelatedTo] = useState<string[]>(
49
+ requirement?.dependencies.relatedTo ?? [],
50
+ );
51
+ const [descriptionText, setDescriptionText] = useState(description ?? "");
52
+
53
+ // User Story
54
+ const [userStoryAs, setUserStoryAs] = useState(
55
+ requirement?.format.type === "user-story" ? requirement.format.userStory.as : "",
56
+ );
57
+ const [userStoryIWant, setUserStoryIWant] = useState(
58
+ requirement?.format.type === "user-story" ? requirement.format.userStory.iWant : "",
59
+ );
60
+ const [userStorySoThat, setUserStorySoThat] = useState(
61
+ requirement?.format.type === "user-story" ? requirement.format.userStory.soThat : "",
62
+ );
63
+
64
+ // EARS
65
+ const [earsType, setEarsType] = useState(
66
+ requirement?.format.type === "ears" ? requirement.format.ears.type : "",
67
+ );
68
+ const [earsTrigger, setEarsTrigger] = useState(
69
+ requirement?.format.type === "ears" ? (requirement.format.ears.trigger ?? "") : "",
70
+ );
71
+ const [earsCondition, setEarsCondition] = useState(
72
+ requirement?.format.type === "ears" ? (requirement.format.ears.condition ?? "") : "",
73
+ );
74
+ const [earsAction, setEarsAction] = useState(
75
+ requirement?.format.type === "ears" ? requirement.format.ears.action : "",
76
+ );
77
+ const [earsResponse, setEarsResponse] = useState(
78
+ requirement?.format.type === "ears" ? (requirement.format.ears.response ?? "") : "",
79
+ );
80
+
81
+ const cancelHref = mode === "edit" ? `/requirements/${requirement!.id}` : "/requirements";
82
+
83
+ return (
84
+ <form action={formAction} className="space-y-6">
85
+ {mode === "edit" ? <input type="hidden" name="id" value={requirement!.id} /> : null}
86
+
87
+ {/* Hidden JSON fields */}
88
+ <input type="hidden" name="successCriteria" value={JSON.stringify(successCriteria)} />
89
+ <input type="hidden" name="blockedBy" value={JSON.stringify(blockedBy)} />
90
+ <input type="hidden" name="blocks" value={JSON.stringify(blocks)} />
91
+ <input type="hidden" name="relatedTo" value={JSON.stringify(relatedTo)} />
92
+ <input type="hidden" name="description" value={descriptionText} />
93
+ <input type="hidden" name="formatType" value={formatType} />
94
+
95
+ {/* User Story hidden fields */}
96
+ <input type="hidden" name="userStoryAs" value={userStoryAs} />
97
+ <input type="hidden" name="userStoryIWant" value={userStoryIWant} />
98
+ <input type="hidden" name="userStorySoThat" value={userStorySoThat} />
99
+
100
+ {/* EARS hidden fields */}
101
+ <input type="hidden" name="earsType" value={earsType} />
102
+ <input type="hidden" name="earsTrigger" value={earsTrigger} />
103
+ <input type="hidden" name="earsCondition" value={earsCondition} />
104
+ <input type="hidden" name="earsAction" value={earsAction} />
105
+ <input type="hidden" name="earsResponse" value={earsResponse} />
106
+
107
+ {!state.success ? (
108
+ <div className="rounded-md bg-red-50 p-4 text-sm text-red-700">
109
+ {state.error}
110
+ </div>
111
+ ) : null}
112
+
113
+ {/* Breadcrumb */}
114
+ <p className="text-sm text-gray-500">
115
+ <Link href="/requirements" className="hover:text-blue-600">
116
+ Requirements
117
+ </Link>
118
+ {mode === "edit" ? (
119
+ <>
120
+ {" / "}
121
+ <Link href={`/requirements/${requirement!.id}`} className="hover:text-blue-600">
122
+ {requirement!.id}
123
+ </Link>
124
+ {" / Edit"}
125
+ </>
126
+ ) : (
127
+ " / New"
128
+ )}
129
+ </p>
130
+
131
+ <h1 className="text-2xl font-bold">
132
+ {mode === "create" ? "New Requirement" : `Edit ${requirement!.id}`}
133
+ </h1>
134
+
135
+ {/* Title */}
136
+ <div>
137
+ <label htmlFor="title" className="block text-sm font-medium text-gray-700">
138
+ Title
139
+ </label>
140
+ <input
141
+ id="title"
142
+ name="title"
143
+ type="text"
144
+ value={title}
145
+ onChange={(e) => setTitle(e.target.value)}
146
+ required
147
+ className="mt-1 w-full 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"
148
+ />
149
+ </div>
150
+
151
+ {/* Status + Priority + Complexity */}
152
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
153
+ <div>
154
+ <label htmlFor="status" className="block text-sm font-medium text-gray-700">
155
+ Status
156
+ </label>
157
+ <select
158
+ id="status"
159
+ name="status"
160
+ value={status}
161
+ onChange={(e) => setStatus(e.target.value)}
162
+ className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm"
163
+ >
164
+ <option value="draft">Draft</option>
165
+ <option value="approved">Approved</option>
166
+ <option value="implemented">Implemented</option>
167
+ <option value="deprecated">Deprecated</option>
168
+ </select>
169
+ </div>
170
+
171
+ <div>
172
+ <label htmlFor="priority" className="block text-sm font-medium text-gray-700">
173
+ Priority
174
+ </label>
175
+ <select
176
+ id="priority"
177
+ name="priority"
178
+ value={priority}
179
+ onChange={(e) => setPriority(e.target.value)}
180
+ className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm"
181
+ >
182
+ <option value="low">Low</option>
183
+ <option value="medium">Medium</option>
184
+ <option value="high">High</option>
185
+ </select>
186
+ </div>
187
+
188
+ <div>
189
+ <label
190
+ htmlFor="estimatedComplexity"
191
+ className="block text-sm font-medium text-gray-700"
192
+ >
193
+ Complexity
194
+ </label>
195
+ <select
196
+ id="estimatedComplexity"
197
+ name="estimatedComplexity"
198
+ value={estimatedComplexity}
199
+ onChange={(e) => setEstimatedComplexity(e.target.value)}
200
+ className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm"
201
+ >
202
+ <option value="">-</option>
203
+ <option value="small">Small</option>
204
+ <option value="medium">Medium</option>
205
+ <option value="large">Large</option>
206
+ <option value="xlarge">XLarge</option>
207
+ </select>
208
+ </div>
209
+ </div>
210
+
211
+ {/* Estimated Hours */}
212
+ <div className="max-w-xs">
213
+ <label htmlFor="estimatedHours" className="block text-sm font-medium text-gray-700">
214
+ Estimated Hours
215
+ </label>
216
+ <input
217
+ id="estimatedHours"
218
+ name="estimatedHours"
219
+ type="number"
220
+ step="0.5"
221
+ min="0"
222
+ value={estimatedHours}
223
+ onChange={(e) => setEstimatedHours(e.target.value)}
224
+ className="mt-1 w-full 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"
225
+ placeholder="Optional"
226
+ />
227
+ </div>
228
+
229
+ {/* Format Type */}
230
+ <div>
231
+ <label className="block text-sm font-medium text-gray-700">Format</label>
232
+ <div className="mt-1 flex gap-4">
233
+ {(["free-form", "user-story", "ears"] as const).map((ft) => (
234
+ <label key={ft} className="flex items-center gap-1.5 text-sm">
235
+ <input
236
+ type="radio"
237
+ name="_formatType"
238
+ value={ft}
239
+ checked={formatType === ft}
240
+ onChange={() => setFormatType(ft)}
241
+ className="border-gray-300"
242
+ />
243
+ {ft === "free-form" ? "Free Form" : ft === "user-story" ? "User Story" : "EARS"}
244
+ </label>
245
+ ))}
246
+ </div>
247
+ </div>
248
+
249
+ {/* User Story Fields */}
250
+ {formatType === "user-story" ? (
251
+ <div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-4">
252
+ <h3 className="text-sm font-medium text-gray-700">User Story</h3>
253
+ <div>
254
+ <label className="block text-sm text-gray-600">As a</label>
255
+ <input
256
+ type="text"
257
+ value={userStoryAs}
258
+ onChange={(e) => setUserStoryAs(e.target.value)}
259
+ className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
260
+ />
261
+ </div>
262
+ <div>
263
+ <label className="block text-sm text-gray-600">I want</label>
264
+ <input
265
+ type="text"
266
+ value={userStoryIWant}
267
+ onChange={(e) => setUserStoryIWant(e.target.value)}
268
+ className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
269
+ />
270
+ </div>
271
+ <div>
272
+ <label className="block text-sm text-gray-600">So that</label>
273
+ <input
274
+ type="text"
275
+ value={userStorySoThat}
276
+ onChange={(e) => setUserStorySoThat(e.target.value)}
277
+ className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
278
+ />
279
+ </div>
280
+ </div>
281
+ ) : null}
282
+
283
+ {/* EARS Fields */}
284
+ {formatType === "ears" ? (
285
+ <div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-4">
286
+ <h3 className="text-sm font-medium text-gray-700">EARS</h3>
287
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
288
+ <div>
289
+ <label className="block text-sm text-gray-600">Type</label>
290
+ <input
291
+ type="text"
292
+ value={earsType}
293
+ onChange={(e) => setEarsType(e.target.value)}
294
+ className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
295
+ />
296
+ </div>
297
+ <div>
298
+ <label className="block text-sm text-gray-600">Trigger</label>
299
+ <input
300
+ type="text"
301
+ value={earsTrigger}
302
+ onChange={(e) => setEarsTrigger(e.target.value)}
303
+ className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
304
+ placeholder="Optional"
305
+ />
306
+ </div>
307
+ <div>
308
+ <label className="block text-sm text-gray-600">Condition</label>
309
+ <input
310
+ type="text"
311
+ value={earsCondition}
312
+ onChange={(e) => setEarsCondition(e.target.value)}
313
+ className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
314
+ placeholder="Optional"
315
+ />
316
+ </div>
317
+ <div>
318
+ <label className="block text-sm text-gray-600">Action</label>
319
+ <input
320
+ type="text"
321
+ value={earsAction}
322
+ onChange={(e) => setEarsAction(e.target.value)}
323
+ className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
324
+ />
325
+ </div>
326
+ <div className="sm:col-span-2">
327
+ <label className="block text-sm text-gray-600">Response</label>
328
+ <input
329
+ type="text"
330
+ value={earsResponse}
331
+ onChange={(e) => setEarsResponse(e.target.value)}
332
+ className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
333
+ placeholder="Optional"
334
+ />
335
+ </div>
336
+ </div>
337
+ </div>
338
+ ) : null}
339
+
340
+ {/* Success Criteria */}
341
+ <SuccessCriteriaEditor criteria={successCriteria} onChange={setSuccessCriteria} />
342
+
343
+ {/* Dependencies */}
344
+ <div className="space-y-4">
345
+ <h3 className="text-sm font-medium text-gray-700">Dependencies</h3>
346
+ <DependencyEditor
347
+ label="Blocked By"
348
+ selected={blockedBy}
349
+ onChange={setBlockedBy}
350
+ allRequirements={allRequirements}
351
+ excludeId={requirement?.id}
352
+ />
353
+ <DependencyEditor
354
+ label="Blocks"
355
+ selected={blocks}
356
+ onChange={setBlocks}
357
+ allRequirements={allRequirements}
358
+ excludeId={requirement?.id}
359
+ />
360
+ <DependencyEditor
361
+ label="Related To"
362
+ selected={relatedTo}
363
+ onChange={setRelatedTo}
364
+ allRequirements={allRequirements}
365
+ excludeId={requirement?.id}
366
+ />
367
+ </div>
368
+
369
+ {/* Description */}
370
+ <MarkdownEditor value={descriptionText} onChange={setDescriptionText} />
371
+
372
+ {/* Actions */}
373
+ <div className="flex items-center gap-3">
374
+ <button
375
+ type="submit"
376
+ disabled={isPending}
377
+ className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
378
+ >
379
+ {isPending ? "Saving..." : mode === "create" ? "Create" : "Save"}
380
+ </button>
381
+ <Link
382
+ href={cancelHref}
383
+ className="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
384
+ >
385
+ Cancel
386
+ </Link>
387
+ </div>
388
+ </form>
389
+ );
390
+ }