prd-gen 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.
- package/README.md +52 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +827 -0
- package/package.json +27 -0
- package/prd.json +187 -0
- package/prd.md +101 -0
- package/progress.txt +118 -0
- package/scripts/ralph/CLAUDE.md +104 -0
- package/scripts/ralph/progress.txt +20 -0
- package/scripts/ralph/ralph.sh +113 -0
- package/src/index.ts +836 -0
- package/tsconfig.json +17 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* prd-gen CLI entry point
|
|
4
|
+
* Single-story-at-a-time PRD review tool
|
|
5
|
+
*/
|
|
6
|
+
import http from 'http';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { exec } from 'child_process';
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
const PORT = 3000;
|
|
14
|
+
const PRD_FILE = path.resolve(process.cwd(), 'prd.json');
|
|
15
|
+
function readStories() {
|
|
16
|
+
try {
|
|
17
|
+
const content = fs.readFileSync(PRD_FILE, 'utf-8');
|
|
18
|
+
const data = JSON.parse(content);
|
|
19
|
+
return data.userStories ?? (Array.isArray(data) ? data : []);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function writeStories(stories) {
|
|
26
|
+
let existing = {};
|
|
27
|
+
try {
|
|
28
|
+
const content = fs.readFileSync(PRD_FILE, 'utf-8');
|
|
29
|
+
existing = JSON.parse(content);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// file doesn't exist or invalid JSON - start fresh
|
|
33
|
+
}
|
|
34
|
+
existing.userStories = stories;
|
|
35
|
+
fs.writeFileSync(PRD_FILE, JSON.stringify(existing, null, 2));
|
|
36
|
+
}
|
|
37
|
+
const HTML_PAGE = `<!DOCTYPE html>
|
|
38
|
+
<html lang="en">
|
|
39
|
+
<head>
|
|
40
|
+
<meta charset="UTF-8" />
|
|
41
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
42
|
+
<title>prd-gen</title>
|
|
43
|
+
<style>
|
|
44
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
45
|
+
|
|
46
|
+
body {
|
|
47
|
+
background: #F2F2F7;
|
|
48
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
49
|
+
min-height: 100vh;
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: center;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#app {
|
|
56
|
+
width: 100%;
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
padding: 40px 20px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.card {
|
|
64
|
+
position: relative;
|
|
65
|
+
background: #FFFFFF;
|
|
66
|
+
border-radius: 16px;
|
|
67
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.10), 0 1px 4px rgba(0,0,0,0.06);
|
|
68
|
+
width: 100%;
|
|
69
|
+
padding: 48px 56px 56px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.story-counter {
|
|
73
|
+
position: absolute;
|
|
74
|
+
top: 20px;
|
|
75
|
+
right: 24px;
|
|
76
|
+
font-size: 13px;
|
|
77
|
+
color: #8E8E93;
|
|
78
|
+
font-weight: 500;
|
|
79
|
+
letter-spacing: 0.01em;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.story-title {
|
|
83
|
+
font-size: 48px;
|
|
84
|
+
font-weight: 700;
|
|
85
|
+
color: #000000;
|
|
86
|
+
line-height: 1.1;
|
|
87
|
+
margin-top: 8px;
|
|
88
|
+
margin-bottom: 24px;
|
|
89
|
+
word-break: break-word;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.story-description {
|
|
93
|
+
font-size: 20px;
|
|
94
|
+
color: #333333;
|
|
95
|
+
line-height: 1.6;
|
|
96
|
+
white-space: pre-wrap;
|
|
97
|
+
word-break: break-word;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.empty-state {
|
|
101
|
+
text-align: center;
|
|
102
|
+
padding: 40px 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.empty-state p {
|
|
106
|
+
font-size: 22px;
|
|
107
|
+
color: #333333;
|
|
108
|
+
margin-bottom: 24px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.add-btn {
|
|
112
|
+
display: inline-flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: 8px;
|
|
115
|
+
background: #FF2D55;
|
|
116
|
+
color: #FFFFFF;
|
|
117
|
+
border: none;
|
|
118
|
+
border-radius: 10px;
|
|
119
|
+
padding: 12px 28px;
|
|
120
|
+
font-size: 16px;
|
|
121
|
+
font-weight: 600;
|
|
122
|
+
cursor: pointer;
|
|
123
|
+
transition: opacity 0.15s;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.add-btn:hover { opacity: 0.85; }
|
|
127
|
+
|
|
128
|
+
.priority-buttons {
|
|
129
|
+
display: flex;
|
|
130
|
+
gap: 8px;
|
|
131
|
+
margin-top: 32px;
|
|
132
|
+
align-items: center;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.priority-btns-left {
|
|
136
|
+
display: flex;
|
|
137
|
+
gap: 8px;
|
|
138
|
+
flex-wrap: wrap;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.priority-btns-right {
|
|
142
|
+
display: flex;
|
|
143
|
+
gap: 8px;
|
|
144
|
+
margin-left: auto;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.priority-btn {
|
|
148
|
+
width: 44px;
|
|
149
|
+
height: 44px;
|
|
150
|
+
border-radius: 50%;
|
|
151
|
+
border: none;
|
|
152
|
+
background: #E5E5EA;
|
|
153
|
+
color: #8E8E93;
|
|
154
|
+
font-size: 16px;
|
|
155
|
+
font-weight: 600;
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
transition: background 0.15s, color 0.15s;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.priority-btn:hover, .priority-btn.selected {
|
|
161
|
+
background: #FF2D55;
|
|
162
|
+
color: #FFFFFF;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.story-title[contenteditable], .story-description[contenteditable] {
|
|
166
|
+
cursor: text;
|
|
167
|
+
outline: none;
|
|
168
|
+
border-radius: 8px;
|
|
169
|
+
padding: 4px 8px;
|
|
170
|
+
margin-left: -8px;
|
|
171
|
+
margin-right: -8px;
|
|
172
|
+
transition: background 0.15s;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.story-title[contenteditable]:hover, .story-description[contenteditable]:hover {
|
|
176
|
+
background: rgba(0,0,0,0.03);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.story-title[contenteditable]:focus, .story-description[contenteditable]:focus {
|
|
180
|
+
background: rgba(0,0,0,0.04);
|
|
181
|
+
outline: 2px solid rgba(255,45,85,0.2);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.action-area {
|
|
185
|
+
display: flex;
|
|
186
|
+
justify-content: center;
|
|
187
|
+
margin-top: 40px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.action-btn {
|
|
191
|
+
border: none;
|
|
192
|
+
border-radius: 12px;
|
|
193
|
+
padding: 14px 40px;
|
|
194
|
+
font-size: 17px;
|
|
195
|
+
font-weight: 600;
|
|
196
|
+
cursor: pointer;
|
|
197
|
+
transition: opacity 0.15s;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.action-btn:hover { opacity: 0.85; }
|
|
201
|
+
|
|
202
|
+
.save-btn {
|
|
203
|
+
background: #FF2D55;
|
|
204
|
+
color: #FFFFFF;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.next-btn {
|
|
208
|
+
background: #E5E5EA;
|
|
209
|
+
color: #8E8E93;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.spinner-ring {
|
|
213
|
+
width: 20px;
|
|
214
|
+
height: 20px;
|
|
215
|
+
border: 2px solid rgba(255,45,85,0.25);
|
|
216
|
+
border-top-color: #FF2D55;
|
|
217
|
+
border-radius: 50%;
|
|
218
|
+
animation: spin 0.7s linear infinite;
|
|
219
|
+
display: inline-block;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@keyframes spin {
|
|
223
|
+
to { transform: rotate(360deg); }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.card-stack {
|
|
227
|
+
position: relative;
|
|
228
|
+
width: 100%;
|
|
229
|
+
max-width: 800px;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.card-ghost {
|
|
233
|
+
position: absolute;
|
|
234
|
+
top: 0;
|
|
235
|
+
left: 0;
|
|
236
|
+
right: 0;
|
|
237
|
+
bottom: 0;
|
|
238
|
+
background: #FFFFFF;
|
|
239
|
+
border-radius: 16px;
|
|
240
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.07), 0 1px 4px rgba(0,0,0,0.04);
|
|
241
|
+
transform-origin: center 90%;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.stack-overflow-badge {
|
|
245
|
+
position: absolute;
|
|
246
|
+
bottom: 14px;
|
|
247
|
+
right: 18px;
|
|
248
|
+
background: rgba(0,0,0,0.10);
|
|
249
|
+
color: #8E8E93;
|
|
250
|
+
border-radius: 10px;
|
|
251
|
+
padding: 2px 9px;
|
|
252
|
+
font-size: 12px;
|
|
253
|
+
font-weight: 600;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.story-action-btn {
|
|
257
|
+
width: 44px;
|
|
258
|
+
height: 44px;
|
|
259
|
+
border-radius: 50%;
|
|
260
|
+
border: none;
|
|
261
|
+
background: #E5E5EA;
|
|
262
|
+
color: #8E8E93;
|
|
263
|
+
font-size: 18px;
|
|
264
|
+
font-weight: 600;
|
|
265
|
+
cursor: pointer;
|
|
266
|
+
display: flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
justify-content: center;
|
|
269
|
+
opacity: 0;
|
|
270
|
+
transition: background 0.15s, color 0.15s, opacity 0.15s;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.card:hover .story-action-btn {
|
|
274
|
+
opacity: 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.story-action-btn:hover {
|
|
278
|
+
background: #FF2D55;
|
|
279
|
+
color: #FFFFFF;
|
|
280
|
+
opacity: 1 !important;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.confirm-overlay {
|
|
284
|
+
position: fixed;
|
|
285
|
+
inset: 0;
|
|
286
|
+
background: rgba(0,0,0,0.45);
|
|
287
|
+
display: flex;
|
|
288
|
+
align-items: center;
|
|
289
|
+
justify-content: center;
|
|
290
|
+
z-index: 100;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.confirm-dialog {
|
|
294
|
+
background: #FFFFFF;
|
|
295
|
+
border-radius: 16px;
|
|
296
|
+
padding: 32px 40px;
|
|
297
|
+
box-shadow: 0 8px 40px rgba(0,0,0,0.18);
|
|
298
|
+
text-align: center;
|
|
299
|
+
max-width: 340px;
|
|
300
|
+
width: 100%;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.confirm-dialog p {
|
|
304
|
+
font-size: 18px;
|
|
305
|
+
color: #000;
|
|
306
|
+
font-weight: 600;
|
|
307
|
+
margin-bottom: 24px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.confirm-actions {
|
|
311
|
+
display: flex;
|
|
312
|
+
gap: 12px;
|
|
313
|
+
justify-content: center;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.confirm-delete-btn {
|
|
317
|
+
background: #FF2D55;
|
|
318
|
+
color: #FFFFFF;
|
|
319
|
+
border: none;
|
|
320
|
+
border-radius: 10px;
|
|
321
|
+
padding: 12px 28px;
|
|
322
|
+
font-size: 16px;
|
|
323
|
+
font-weight: 600;
|
|
324
|
+
cursor: pointer;
|
|
325
|
+
transition: opacity 0.15s;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.confirm-delete-btn:hover { opacity: 0.85; }
|
|
329
|
+
|
|
330
|
+
.confirm-cancel-btn {
|
|
331
|
+
background: #E5E5EA;
|
|
332
|
+
color: #333;
|
|
333
|
+
border: none;
|
|
334
|
+
border-radius: 10px;
|
|
335
|
+
padding: 12px 28px;
|
|
336
|
+
font-size: 16px;
|
|
337
|
+
font-weight: 600;
|
|
338
|
+
cursor: pointer;
|
|
339
|
+
transition: opacity 0.15s;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.confirm-cancel-btn:hover { opacity: 0.85; }
|
|
343
|
+
|
|
344
|
+
.finish-btn {
|
|
345
|
+
background: #E5E5EA;
|
|
346
|
+
color: #AEAEB2;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.summary-state {
|
|
350
|
+
text-align: center;
|
|
351
|
+
padding: 32px 0 16px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.summary-check {
|
|
355
|
+
width: 64px;
|
|
356
|
+
height: 64px;
|
|
357
|
+
border-radius: 50%;
|
|
358
|
+
background: #FF2D55;
|
|
359
|
+
color: #FFFFFF;
|
|
360
|
+
display: flex;
|
|
361
|
+
align-items: center;
|
|
362
|
+
justify-content: center;
|
|
363
|
+
margin: 0 auto 24px;
|
|
364
|
+
font-size: 32px;
|
|
365
|
+
font-weight: 700;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.summary-heading {
|
|
369
|
+
font-size: 40px;
|
|
370
|
+
font-weight: 700;
|
|
371
|
+
color: #000;
|
|
372
|
+
margin-bottom: 8px;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.summary-subheading {
|
|
376
|
+
font-size: 20px;
|
|
377
|
+
color: #555;
|
|
378
|
+
margin-bottom: 36px;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.summary-stats {
|
|
382
|
+
display: flex;
|
|
383
|
+
justify-content: center;
|
|
384
|
+
gap: 48px;
|
|
385
|
+
margin-bottom: 40px;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.summary-stat-value {
|
|
389
|
+
font-size: 40px;
|
|
390
|
+
font-weight: 700;
|
|
391
|
+
color: #FF2D55;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.summary-stat-label {
|
|
395
|
+
font-size: 14px;
|
|
396
|
+
color: #8E8E93;
|
|
397
|
+
font-weight: 500;
|
|
398
|
+
margin-top: 4px;
|
|
399
|
+
text-transform: uppercase;
|
|
400
|
+
letter-spacing: 0.05em;
|
|
401
|
+
}
|
|
402
|
+
</style>
|
|
403
|
+
</head>
|
|
404
|
+
<body>
|
|
405
|
+
<div id="app"></div>
|
|
406
|
+
<script>
|
|
407
|
+
(function() {
|
|
408
|
+
const app = document.getElementById('app');
|
|
409
|
+
let stories = [];
|
|
410
|
+
let currentIndex = 0;
|
|
411
|
+
let dirty = false;
|
|
412
|
+
let saving = false;
|
|
413
|
+
let showDeleteConfirm = false;
|
|
414
|
+
let showSummary = false;
|
|
415
|
+
let createdCount = 0;
|
|
416
|
+
let deletedCount = 0;
|
|
417
|
+
|
|
418
|
+
function render() {
|
|
419
|
+
if (stories.length === 0) {
|
|
420
|
+
app.innerHTML = \`
|
|
421
|
+
<div class="card">
|
|
422
|
+
<div class="empty-state">
|
|
423
|
+
<p>No stories to review.</p>
|
|
424
|
+
<button class="add-btn" id="addBtn">+ Add</button>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
\`;
|
|
428
|
+
document.getElementById('addBtn').addEventListener('click', addStory);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (showSummary) {
|
|
433
|
+
app.innerHTML = \`
|
|
434
|
+
<div class="card-stack">
|
|
435
|
+
<div class="card">
|
|
436
|
+
<div class="summary-state">
|
|
437
|
+
<div class="summary-check">✓</div>
|
|
438
|
+
<h2 class="summary-heading">Good job!</h2>
|
|
439
|
+
<p class="summary-subheading">You've reviewed all stories.</p>
|
|
440
|
+
<div class="summary-stats">
|
|
441
|
+
<div class="summary-stat">
|
|
442
|
+
<div class="summary-stat-value">\${stories.length}</div>
|
|
443
|
+
<div class="summary-stat-label">Reviewed</div>
|
|
444
|
+
</div>
|
|
445
|
+
<div class="summary-stat">
|
|
446
|
+
<div class="summary-stat-value">\${createdCount}</div>
|
|
447
|
+
<div class="summary-stat-label">Created</div>
|
|
448
|
+
</div>
|
|
449
|
+
<div class="summary-stat">
|
|
450
|
+
<div class="summary-stat-value">\${deletedCount}</div>
|
|
451
|
+
<div class="summary-stat-label">Deleted</div>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
<button class="add-btn" id="addSummaryBtn">+ Add new story</button>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
\`;
|
|
459
|
+
document.getElementById('addSummaryBtn').addEventListener('click', addStoryAtEnd);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const story = stories[currentIndex];
|
|
464
|
+
const title = story.title || '';
|
|
465
|
+
const description = story.description || '';
|
|
466
|
+
const priority = story.priority != null ? story.priority : null;
|
|
467
|
+
const x = currentIndex + 1;
|
|
468
|
+
const y = stories.length;
|
|
469
|
+
|
|
470
|
+
const buttonsHtml = [1,2,3,4,5,6,7,8,9,10].map(function(n) {
|
|
471
|
+
const sel = priority === n ? ' selected' : '';
|
|
472
|
+
return \`<button class="priority-btn\${sel}" data-priority="\${n}">\${n}</button>\`;
|
|
473
|
+
}).join('');
|
|
474
|
+
|
|
475
|
+
var isLastStory = currentIndex === stories.length - 1;
|
|
476
|
+
const actionHtml = saving
|
|
477
|
+
? \`<div class="action-area"><span class="spinner-ring"></span></div>\`
|
|
478
|
+
: \`<div class="action-area"><button class="action-btn \${dirty ? 'save-btn' : (isLastStory ? 'finish-btn' : 'next-btn')}" id="actionBtn">\${dirty ? 'Save' : (isLastStory ? 'Finish' : 'Next story')}</button></div>\`;
|
|
479
|
+
|
|
480
|
+
var remaining = stories.length - currentIndex - 1;
|
|
481
|
+
var visibleGhosts, overflow;
|
|
482
|
+
if (remaining <= 5) {
|
|
483
|
+
visibleGhosts = remaining;
|
|
484
|
+
overflow = 0;
|
|
485
|
+
} else if (remaining <= 15) {
|
|
486
|
+
visibleGhosts = 5;
|
|
487
|
+
overflow = remaining - 5;
|
|
488
|
+
} else {
|
|
489
|
+
visibleGhosts = 3;
|
|
490
|
+
overflow = remaining - 3;
|
|
491
|
+
}
|
|
492
|
+
var rotationSets = { 0: [], 1: [3], 2: [5, 2], 3: [6, 3, 1.5], 4: [7, 4.5, 2.5, 1], 5: [8, 5.5, 3.5, 2, 0.5] };
|
|
493
|
+
var rotations = rotationSets[visibleGhosts] || [];
|
|
494
|
+
var ghostHtml = '';
|
|
495
|
+
for (var gi = 0; gi < visibleGhosts; gi++) {
|
|
496
|
+
var rot = rotations[gi];
|
|
497
|
+
var badge = (gi === 0 && overflow > 0) ? \`<span class="stack-overflow-badge">+\${overflow}</span>\` : '';
|
|
498
|
+
ghostHtml += \`<div class="card-ghost" style="transform: rotate(\${rot}deg);">\${badge}</div>\`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
app.innerHTML = \`
|
|
502
|
+
<div class="card-stack">
|
|
503
|
+
\${ghostHtml}
|
|
504
|
+
<div class="card">
|
|
505
|
+
<span class="story-counter">Story \${x} / \${y}</span>
|
|
506
|
+
<div class="story-title" contenteditable="true" id="titleField" spellcheck="false">\${escapeHtml(title)}</div>
|
|
507
|
+
<div class="story-description" contenteditable="true" id="descField">\${escapeHtml(description)}</div>
|
|
508
|
+
<div class="priority-buttons">
|
|
509
|
+
<div class="priority-btns-left">\${buttonsHtml}</div>
|
|
510
|
+
<div class="priority-btns-right">
|
|
511
|
+
<button class="story-action-btn" id="addStoryBtn" title="Add story before this one">+</button>
|
|
512
|
+
<button class="story-action-btn" id="deleteStoryBtn" title="Delete this story"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M1 3.5h12"/><path d="M4.5 3.5v-2a.5.5 0 01.5-.5h4a.5.5 0 01.5.5v2"/><path d="M2.5 3.5l.75 8.5a.75.75 0 00.75.5h6a.75.75 0 00.75-.5l.75-8.5"/><path d="M5.5 6.5v4M8.5 6.5v4"/></svg></button>
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
\${actionHtml}
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
\${showDeleteConfirm ? \`<div class="confirm-overlay" id="confirmOverlay">
|
|
519
|
+
<div class="confirm-dialog">
|
|
520
|
+
<p>Delete this story? No undo.</p>
|
|
521
|
+
<div class="confirm-actions">
|
|
522
|
+
<button class="confirm-delete-btn" id="confirmDeleteBtn">Delete</button>
|
|
523
|
+
<button class="confirm-cancel-btn" id="confirmCancelBtn">Cancel</button>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
</div>\` : ''}
|
|
527
|
+
\`;
|
|
528
|
+
|
|
529
|
+
var addStoryBtn = document.getElementById('addStoryBtn');
|
|
530
|
+
if (addStoryBtn) {
|
|
531
|
+
addStoryBtn.addEventListener('click', addStoryBefore);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
var deleteStoryBtn = document.getElementById('deleteStoryBtn');
|
|
535
|
+
if (deleteStoryBtn) {
|
|
536
|
+
deleteStoryBtn.addEventListener('click', function() {
|
|
537
|
+
showDeleteConfirm = true;
|
|
538
|
+
render();
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (showDeleteConfirm) {
|
|
543
|
+
var confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
|
|
544
|
+
var confirmCancelBtn = document.getElementById('confirmCancelBtn');
|
|
545
|
+
if (confirmDeleteBtn) {
|
|
546
|
+
confirmDeleteBtn.addEventListener('click', function() {
|
|
547
|
+
showDeleteConfirm = false;
|
|
548
|
+
deleteStory();
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (confirmCancelBtn) {
|
|
552
|
+
confirmCancelBtn.addEventListener('click', function() {
|
|
553
|
+
showDeleteConfirm = false;
|
|
554
|
+
render();
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
document.querySelectorAll('.priority-btn').forEach(function(btn) {
|
|
560
|
+
btn.addEventListener('click', function() {
|
|
561
|
+
stories[currentIndex].priority = parseInt(btn.getAttribute('data-priority'), 10);
|
|
562
|
+
dirty = true;
|
|
563
|
+
render();
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
var titleField = document.getElementById('titleField');
|
|
568
|
+
var descField = document.getElementById('descField');
|
|
569
|
+
|
|
570
|
+
if (titleField) {
|
|
571
|
+
titleField.addEventListener('input', function() {
|
|
572
|
+
stories[currentIndex].title = titleField.textContent || '';
|
|
573
|
+
dirty = true;
|
|
574
|
+
});
|
|
575
|
+
titleField.addEventListener('keydown', function(e) {
|
|
576
|
+
if (e.key === 'Escape') { titleField.blur(); }
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (descField) {
|
|
581
|
+
descField.addEventListener('input', function() {
|
|
582
|
+
stories[currentIndex].description = descField.innerText || '';
|
|
583
|
+
dirty = true;
|
|
584
|
+
});
|
|
585
|
+
descField.addEventListener('keydown', function(e) {
|
|
586
|
+
if (e.key === 'Escape') { descField.blur(); }
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
var actionBtn = document.getElementById('actionBtn');
|
|
591
|
+
if (actionBtn) {
|
|
592
|
+
if (dirty) {
|
|
593
|
+
actionBtn.addEventListener('click', function() {
|
|
594
|
+
saving = true;
|
|
595
|
+
render();
|
|
596
|
+
fetch('/api/stories', {
|
|
597
|
+
method: 'POST',
|
|
598
|
+
headers: { 'Content-Type': 'application/json' },
|
|
599
|
+
body: JSON.stringify(stories)
|
|
600
|
+
}).then(function() {
|
|
601
|
+
dirty = false;
|
|
602
|
+
saving = false;
|
|
603
|
+
render();
|
|
604
|
+
}).catch(function() {
|
|
605
|
+
saving = false;
|
|
606
|
+
render();
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
} else {
|
|
610
|
+
actionBtn.addEventListener('click', function() {
|
|
611
|
+
if (currentIndex < stories.length - 1) {
|
|
612
|
+
currentIndex++;
|
|
613
|
+
render();
|
|
614
|
+
} else {
|
|
615
|
+
showSummary = true;
|
|
616
|
+
render();
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function escapeHtml(str) {
|
|
624
|
+
return String(str)
|
|
625
|
+
.replace(/&/g, '&')
|
|
626
|
+
.replace(/</g, '<')
|
|
627
|
+
.replace(/>/g, '>')
|
|
628
|
+
.replace(/"/g, '"');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function addStory() {
|
|
632
|
+
const newStory = { title: 'New Item', description: 'Describe here\u2026', priority: null };
|
|
633
|
+
stories.splice(currentIndex, 0, newStory);
|
|
634
|
+
createdCount++;
|
|
635
|
+
saveAndRender();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function addStoryBefore() {
|
|
639
|
+
const newStory = { title: 'New Item', description: 'Describe here\u2026', priority: null };
|
|
640
|
+
stories.splice(currentIndex, 0, newStory);
|
|
641
|
+
currentIndex++;
|
|
642
|
+
createdCount++;
|
|
643
|
+
saveAndRender();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function addStoryAtEnd() {
|
|
647
|
+
const newStory = { title: 'New Item', description: 'Describe here\u2026', priority: null };
|
|
648
|
+
stories.push(newStory);
|
|
649
|
+
currentIndex = stories.length - 1;
|
|
650
|
+
showSummary = false;
|
|
651
|
+
createdCount++;
|
|
652
|
+
saveAndRender();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function deleteStory() {
|
|
656
|
+
stories.splice(currentIndex, 1);
|
|
657
|
+
if (currentIndex >= stories.length && currentIndex > 0) {
|
|
658
|
+
currentIndex = stories.length - 1;
|
|
659
|
+
}
|
|
660
|
+
deletedCount++;
|
|
661
|
+
saveAndRender();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function saveAndRender() {
|
|
665
|
+
fetch('/api/stories', {
|
|
666
|
+
method: 'POST',
|
|
667
|
+
headers: { 'Content-Type': 'application/json' },
|
|
668
|
+
body: JSON.stringify(stories)
|
|
669
|
+
}).then(function() {
|
|
670
|
+
dirty = false;
|
|
671
|
+
render();
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function isEditingText() {
|
|
676
|
+
var el = document.activeElement;
|
|
677
|
+
if (!el) return false;
|
|
678
|
+
var tag = el.tagName.toLowerCase();
|
|
679
|
+
if (tag === 'input' || tag === 'textarea') return true;
|
|
680
|
+
if (el.getAttribute('contenteditable') === 'true') return true;
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function handleKeyDown(e) {
|
|
685
|
+
// Esc: close dialog or blur active field
|
|
686
|
+
if (e.key === 'Escape') {
|
|
687
|
+
if (showDeleteConfirm) {
|
|
688
|
+
showDeleteConfirm = false;
|
|
689
|
+
render();
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (document.activeElement && document.activeElement !== document.body) {
|
|
693
|
+
document.activeElement.blur();
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// All other shortcuts are ignored when editing text
|
|
699
|
+
if (isEditingText()) return;
|
|
700
|
+
|
|
701
|
+
// No stories or showing summary — nothing to act on
|
|
702
|
+
if (stories.length === 0 || showSummary) return;
|
|
703
|
+
|
|
704
|
+
// Priority keys 1–9
|
|
705
|
+
if (e.key >= '1' && e.key <= '9') {
|
|
706
|
+
stories[currentIndex].priority = parseInt(e.key, 10);
|
|
707
|
+
dirty = true;
|
|
708
|
+
render();
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Key 0 → priority 10
|
|
713
|
+
if (e.key === '0') {
|
|
714
|
+
stories[currentIndex].priority = 10;
|
|
715
|
+
dirty = true;
|
|
716
|
+
render();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Enter or Space → Save (dirty) or Next story (clean)
|
|
721
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
722
|
+
e.preventDefault();
|
|
723
|
+
if (!saving) {
|
|
724
|
+
var actionBtn = document.getElementById('actionBtn');
|
|
725
|
+
if (actionBtn) actionBtn.click();
|
|
726
|
+
}
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Backspace or Delete → open delete confirmation
|
|
731
|
+
if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
732
|
+
if (!saving) {
|
|
733
|
+
showDeleteConfirm = true;
|
|
734
|
+
render();
|
|
735
|
+
}
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// N → insert new story before current
|
|
740
|
+
if (e.key === 'n' || e.key === 'N') {
|
|
741
|
+
if (!saving) addStoryBefore();
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
747
|
+
|
|
748
|
+
fetch('/api/stories')
|
|
749
|
+
.then(function(r) { return r.json(); })
|
|
750
|
+
.then(function(data) {
|
|
751
|
+
stories = Array.isArray(data) ? data : [];
|
|
752
|
+
render();
|
|
753
|
+
})
|
|
754
|
+
.catch(function() {
|
|
755
|
+
stories = [];
|
|
756
|
+
render();
|
|
757
|
+
});
|
|
758
|
+
})();
|
|
759
|
+
</script>
|
|
760
|
+
</body>
|
|
761
|
+
</html>`;
|
|
762
|
+
const server = http.createServer((req, res) => {
|
|
763
|
+
const url = req.url ?? '/';
|
|
764
|
+
const method = req.method ?? 'GET';
|
|
765
|
+
if (url === '/' && method === 'GET') {
|
|
766
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
767
|
+
res.end(HTML_PAGE);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (url === '/api/stories' && method === 'GET') {
|
|
771
|
+
const stories = readStories();
|
|
772
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
773
|
+
res.end(JSON.stringify(stories));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (url === '/api/stories' && method === 'POST') {
|
|
777
|
+
let body = '';
|
|
778
|
+
req.on('data', (chunk) => {
|
|
779
|
+
body += chunk.toString();
|
|
780
|
+
});
|
|
781
|
+
req.on('end', () => {
|
|
782
|
+
try {
|
|
783
|
+
const stories = JSON.parse(body);
|
|
784
|
+
writeStories(stories);
|
|
785
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
786
|
+
res.end(JSON.stringify({ ok: true }));
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
790
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
796
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
797
|
+
});
|
|
798
|
+
server.listen(PORT, () => {
|
|
799
|
+
const url = `http://localhost:${PORT}`;
|
|
800
|
+
console.log(`prd-gen running at ${url}`);
|
|
801
|
+
openBrowser(url);
|
|
802
|
+
});
|
|
803
|
+
function openBrowser(url) {
|
|
804
|
+
const platform = process.platform;
|
|
805
|
+
let command;
|
|
806
|
+
if (platform === 'darwin') {
|
|
807
|
+
command = `open "${url}"`;
|
|
808
|
+
}
|
|
809
|
+
else if (platform === 'win32') {
|
|
810
|
+
command = `start "${url}"`;
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
command = `xdg-open "${url}"`;
|
|
814
|
+
}
|
|
815
|
+
exec(command, (err) => {
|
|
816
|
+
if (err) {
|
|
817
|
+
console.error('Could not open browser automatically:', err.message);
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
process.on('SIGINT', () => {
|
|
822
|
+
console.log('\nShutting down...');
|
|
823
|
+
server.close(() => {
|
|
824
|
+
process.exit(0);
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
//# sourceMappingURL=index.js.map
|