design-share 0.3.0 → 0.3.1
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 +4 -0
- package/package.json +7 -1
- package/public/app.js +75 -0
- package/public/index.html +12 -0
- package/public/style.css +105 -5
- package/src/github.js +31 -0
- package/src/server.js +14 -1
package/README.md
CHANGED
|
@@ -62,3 +62,7 @@ Quitting with Ctrl+C in the terminal is respected as a deliberate stop. Closing
|
|
|
62
62
|
## Requirements
|
|
63
63
|
|
|
64
64
|
Node 18 or newer, git, and a repo with an origin remote (solo local mode works without one).
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
Built by [Callebe](https://github.com/callebe01). If design-share saves your team some friction, you can [buy me a coffee](https://buymeacoffee.com/callebe).
|
package/package.json
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "design-share",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Live branch previews and pinned comments for design teams. Zero config, no server, git is the backend.",
|
|
5
5
|
"keywords": ["design", "preview", "branches", "collaboration", "cli"],
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"funding": "https://buymeacoffee.com/callebe",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/callebe01/design-share.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/callebe01/design-share#readme",
|
|
7
13
|
"type": "module",
|
|
8
14
|
"bin": {
|
|
9
15
|
"design-share": "bin/design-share.js"
|
package/public/app.js
CHANGED
|
@@ -10,6 +10,62 @@ const icons = {
|
|
|
10
10
|
eye: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>',
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
/* ---------- PR panel (read only) ---------- */
|
|
14
|
+
|
|
15
|
+
function renderPrChip() {
|
|
16
|
+
const chip = el('pr-chip');
|
|
17
|
+
const sel = state.selected;
|
|
18
|
+
const pr = sel && state.board.prs ? state.board.prs[sel.branch] : null;
|
|
19
|
+
chip.hidden = !pr;
|
|
20
|
+
if (pr) {
|
|
21
|
+
const open = !el('pr-panel').hidden;
|
|
22
|
+
chip.innerHTML = `PR #${pr.number} <span class="chev">${open ? '▴' : '▾'}</span>`;
|
|
23
|
+
chip.title = pr.title || '';
|
|
24
|
+
} else {
|
|
25
|
+
el('pr-panel').hidden = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function togglePrPanel(force) {
|
|
30
|
+
const panel = el('pr-panel');
|
|
31
|
+
const show = force !== undefined ? force : panel.hidden;
|
|
32
|
+
if (!show) { panel.hidden = true; renderPrChip(); return; }
|
|
33
|
+
const sel = state.selected;
|
|
34
|
+
if (!sel) return;
|
|
35
|
+
panel.innerHTML = '<div class="pr-loading"><div class="spinner"></div></div>';
|
|
36
|
+
panel.hidden = false;
|
|
37
|
+
renderPrChip();
|
|
38
|
+
const r = await fetch(`/api/pr?branch=${encodeURIComponent(sel.branch)}`).then((x) => x.json());
|
|
39
|
+
if (panel.hidden) return; // closed while loading
|
|
40
|
+
if (!r.pr) {
|
|
41
|
+
panel.innerHTML = '<div class="pr-loading">Could not load the PR right now.</div>';
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const pr = r.pr;
|
|
45
|
+
const stateCls = pr.state === 'OPEN' ? 'live' : pr.state === 'MERGED' ? 'merged' : 'closed';
|
|
46
|
+
panel.innerHTML = `
|
|
47
|
+
<div class="pr-head">
|
|
48
|
+
<span class="dot ${stateCls}"></span>
|
|
49
|
+
<span class="pr-title">${escapeHtml(pr.title)}</span>
|
|
50
|
+
<span class="pr-num">#${pr.number}</span>
|
|
51
|
+
<a class="pr-open" href="${escapeHtml(pr.url)}" target="_blank" rel="noopener">Open on GitHub</a>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="pr-byline">${escapeHtml(pr.author)} · ${escapeHtml(pr.state.toLowerCase())} · ${timeAgo(Date.parse(pr.createdAt) / 1000)}</div>
|
|
54
|
+
${pr.body ? `<div class="pr-body">${escapeHtml(pr.body)}</div>` : '<div class="pr-body dim-note">No description.</div>'}
|
|
55
|
+
${pr.comments.length ? `
|
|
56
|
+
<div class="pr-comments-label">Comments (${pr.comments.length})</div>
|
|
57
|
+
${pr.comments.map((c) => `
|
|
58
|
+
<div class="pr-comment">
|
|
59
|
+
<div class="pr-comment-head">
|
|
60
|
+
<span class="avatar">${escapeHtml((c.author || '?').slice(0, 1))}</span>
|
|
61
|
+
<b>${escapeHtml(c.author)}</b>
|
|
62
|
+
<span class="pr-comment-time">${timeAgo(Date.parse(c.createdAt) / 1000)}</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="pr-comment-body">${escapeHtml(c.body)}</div>
|
|
65
|
+
</div>`).join('')}` : '<div class="dim-note" style="margin-top:8px">No comments on the PR yet.</div>'}
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
|
|
13
69
|
/* ---------- review requests ---------- */
|
|
14
70
|
|
|
15
71
|
function branchRequests(branch) {
|
|
@@ -271,6 +327,7 @@ function branchRow({ user, branch, time, dim, own }) {
|
|
|
271
327
|
|
|
272
328
|
async function select(user, branch) {
|
|
273
329
|
state.selected = { user, branch };
|
|
330
|
+
el('pr-panel').hidden = true;
|
|
274
331
|
state.commentMode = false;
|
|
275
332
|
state.pendingPin = null;
|
|
276
333
|
state.focusedComment = null;
|
|
@@ -358,6 +415,7 @@ function renderStage() {
|
|
|
358
415
|
}
|
|
359
416
|
|
|
360
417
|
renderRequestButton();
|
|
418
|
+
renderPrChip();
|
|
361
419
|
|
|
362
420
|
const cm = el('btn-comment-mode');
|
|
363
421
|
cm.innerHTML = `${icons.message} <span>${state.commentMode ? 'Click an element to pin' : 'Comment'}</span>`;
|
|
@@ -668,6 +726,7 @@ el('btn-retry').onclick = () => pollPreview();
|
|
|
668
726
|
document.addEventListener('keydown', (e) => {
|
|
669
727
|
if (e.key === 'Escape') {
|
|
670
728
|
if (!el('composer').hidden) closeComposer();
|
|
729
|
+
else if (!el('pr-panel').hidden) togglePrPanel(false);
|
|
671
730
|
else if (state.commentMode) { state.commentMode = false; renderStage(); }
|
|
672
731
|
else if (state.focusedComment) {
|
|
673
732
|
state.focusedComment = null;
|
|
@@ -722,9 +781,25 @@ function parseHash() {
|
|
|
722
781
|
state.railFilter = e.target.value;
|
|
723
782
|
renderRail();
|
|
724
783
|
});
|
|
784
|
+
const setRailCollapsed = (collapsed) => {
|
|
785
|
+
document.getElementById('app').classList.toggle('rail-collapsed', collapsed);
|
|
786
|
+
el('btn-expand').hidden = !collapsed;
|
|
787
|
+
try { localStorage.setItem('ds-rail-collapsed', collapsed ? '1' : ''); } catch { /* private mode */ }
|
|
788
|
+
};
|
|
789
|
+
el('btn-collapse').onclick = () => setRailCollapsed(true);
|
|
790
|
+
el('btn-expand').onclick = () => setRailCollapsed(false);
|
|
791
|
+
try { if (localStorage.getItem('ds-rail-collapsed') === '1') setRailCollapsed(true); } catch { /* private mode */ }
|
|
792
|
+
document.addEventListener('keydown', (e) => {
|
|
793
|
+
if (e.key === '[' && !isTyping()) {
|
|
794
|
+
setRailCollapsed(!document.getElementById('app').classList.contains('rail-collapsed'));
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
|
|
725
798
|
el('btn-request').onclick = (e) => { e.stopPropagation(); toggleRequestPopover(); };
|
|
799
|
+
el('pr-chip').onclick = (e) => { e.stopPropagation(); togglePrPanel(); };
|
|
726
800
|
document.addEventListener('click', (e) => {
|
|
727
801
|
if (!el('request-popover').hidden && !e.target.closest('.request-wrap')) toggleRequestPopover(false);
|
|
802
|
+
if (!el('pr-panel').hidden && !e.target.closest('#pr-panel') && !e.target.closest('#pr-chip')) togglePrPanel(false);
|
|
728
803
|
});
|
|
729
804
|
renderViewportSeg();
|
|
730
805
|
await refresh();
|
package/public/index.html
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
<div class="rail-head">
|
|
13
13
|
<span class="logo-mark"></span>
|
|
14
14
|
<span id="repo-name" class="repo-name">…</span>
|
|
15
|
+
<button id="btn-collapse" class="icon-btn" title="Collapse sidebar [">
|
|
16
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>
|
|
17
|
+
</button>
|
|
15
18
|
</div>
|
|
16
19
|
<div class="rail-filter">
|
|
17
20
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
|
@@ -20,14 +23,22 @@
|
|
|
20
23
|
<div id="rail-sections"></div>
|
|
21
24
|
<div class="rail-foot">
|
|
22
25
|
<div id="sync-status" class="sync-status"></div>
|
|
26
|
+
<a class="coffee-link" href="https://buymeacoffee.com/callebe" target="_blank" rel="noopener">
|
|
27
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 2v2"/><path d="M14 2v2"/><path d="M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1"/><path d="M6 2v2"/></svg>
|
|
28
|
+
buy me a coffee
|
|
29
|
+
</a>
|
|
23
30
|
</div>
|
|
24
31
|
</aside>
|
|
25
32
|
|
|
26
33
|
<main id="stage">
|
|
27
34
|
<header id="stage-head">
|
|
28
35
|
<div class="stage-title-wrap">
|
|
36
|
+
<button id="btn-expand" class="icon-btn" title="Show sidebar [" hidden>
|
|
37
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>
|
|
38
|
+
</button>
|
|
29
39
|
<span id="stage-title" class="stage-title">pick a branch</span>
|
|
30
40
|
<span id="stage-meta" class="stage-meta"></span>
|
|
41
|
+
<button id="pr-chip" class="pr-chip" hidden></button>
|
|
31
42
|
</div>
|
|
32
43
|
<div class="stage-actions">
|
|
33
44
|
<div class="seg" id="viewport-seg"></div>
|
|
@@ -38,6 +49,7 @@
|
|
|
38
49
|
<button class="btn" id="btn-comment-mode"></button>
|
|
39
50
|
</div>
|
|
40
51
|
</header>
|
|
52
|
+
<div id="pr-panel" hidden></div>
|
|
41
53
|
<div id="frame-area">
|
|
42
54
|
<div id="frame-wrap">
|
|
43
55
|
<div id="frame-empty" class="frame-state">
|
package/public/style.css
CHANGED
|
@@ -65,7 +65,23 @@ button { font: inherit; cursor: pointer; }
|
|
|
65
65
|
background: #fff;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
.repo-name { font-size: 13.5px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
68
|
+
.repo-name { font-size: 13.5px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
69
|
+
|
|
70
|
+
.icon-btn {
|
|
71
|
+
display: inline-flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
width: 24px;
|
|
75
|
+
height: 24px;
|
|
76
|
+
border: 0;
|
|
77
|
+
border-radius: var(--radius);
|
|
78
|
+
background: transparent;
|
|
79
|
+
color: var(--text-3);
|
|
80
|
+
flex: none;
|
|
81
|
+
}
|
|
82
|
+
.icon-btn:hover { color: var(--text); background: #f2f2f4; }
|
|
83
|
+
|
|
84
|
+
#app.rail-collapsed #rail { display: none; }
|
|
69
85
|
|
|
70
86
|
.rail-filter {
|
|
71
87
|
display: flex;
|
|
@@ -137,6 +153,75 @@ button { font: inherit; cursor: pointer; }
|
|
|
137
153
|
|
|
138
154
|
.badge.review { color: var(--amber); border-color: #ecd9b8; background: #fdf8ee; }
|
|
139
155
|
|
|
156
|
+
.pr-chip {
|
|
157
|
+
display: inline-flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
gap: 4px;
|
|
160
|
+
font-size: 10.5px;
|
|
161
|
+
color: var(--text-2);
|
|
162
|
+
border: 1px solid var(--border);
|
|
163
|
+
border-radius: 99px;
|
|
164
|
+
padding: 0 8px;
|
|
165
|
+
line-height: 17px;
|
|
166
|
+
background: #fff;
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
flex: none;
|
|
169
|
+
}
|
|
170
|
+
.pr-chip:hover { color: var(--text); border-color: #d0d0d5; }
|
|
171
|
+
.pr-chip .chev { font-size: 8px; color: var(--text-3); }
|
|
172
|
+
|
|
173
|
+
#pr-panel {
|
|
174
|
+
position: absolute;
|
|
175
|
+
top: 46px;
|
|
176
|
+
left: 14px;
|
|
177
|
+
width: min(560px, calc(100% - 28px));
|
|
178
|
+
max-height: 55vh;
|
|
179
|
+
overflow-y: auto;
|
|
180
|
+
background: var(--panel);
|
|
181
|
+
border: 1px solid var(--border);
|
|
182
|
+
border-radius: 0 0 10px 10px;
|
|
183
|
+
border-top: 0;
|
|
184
|
+
box-shadow: var(--shadow);
|
|
185
|
+
padding: 12px 14px;
|
|
186
|
+
z-index: 15;
|
|
187
|
+
}
|
|
188
|
+
#stage { position: relative; }
|
|
189
|
+
|
|
190
|
+
.pr-loading { display: flex; justify-content: center; padding: 18px; color: var(--text-3); font-size: 12px; }
|
|
191
|
+
.pr-head { display: flex; align-items: center; gap: 8px; }
|
|
192
|
+
.pr-head .dot.merged { background: #8250df; }
|
|
193
|
+
.pr-head .dot.closed { background: var(--red); }
|
|
194
|
+
.pr-title { font-size: 13.5px; font-weight: 600; flex: 1; min-width: 0; }
|
|
195
|
+
.pr-num { font-size: 11.5px; color: var(--text-3); flex: none; }
|
|
196
|
+
.pr-open { font-size: 11.5px; flex: none; color: var(--text-2); text-decoration: none; border: 1px solid var(--border); border-radius: var(--radius); padding: 2px 8px; }
|
|
197
|
+
.pr-open:hover { color: var(--text); border-color: #d0d0d5; }
|
|
198
|
+
.pr-byline { font-size: 11.5px; color: var(--text-3); margin: 4px 0 8px; }
|
|
199
|
+
.pr-body {
|
|
200
|
+
font-size: 12.5px;
|
|
201
|
+
color: var(--text);
|
|
202
|
+
white-space: pre-wrap;
|
|
203
|
+
word-break: break-word;
|
|
204
|
+
border: 1px solid var(--border);
|
|
205
|
+
border-radius: 8px;
|
|
206
|
+
padding: 9px 11px;
|
|
207
|
+
background: #fafafa;
|
|
208
|
+
max-height: 220px;
|
|
209
|
+
overflow-y: auto;
|
|
210
|
+
}
|
|
211
|
+
.dim-note { font-size: 12px; color: var(--text-3); }
|
|
212
|
+
.pr-comments-label {
|
|
213
|
+
font-size: 10.5px;
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
letter-spacing: 0.06em;
|
|
216
|
+
text-transform: uppercase;
|
|
217
|
+
color: var(--text-3);
|
|
218
|
+
margin: 12px 0 6px;
|
|
219
|
+
}
|
|
220
|
+
.pr-comment { border-top: 1px solid var(--border); padding: 8px 0; }
|
|
221
|
+
.pr-comment-head { display: flex; align-items: center; gap: 7px; font-size: 12px; margin-bottom: 3px; }
|
|
222
|
+
.pr-comment-time { font-size: 10.5px; color: var(--text-3); }
|
|
223
|
+
.pr-comment-body { font-size: 12.5px; color: var(--text-2); white-space: pre-wrap; word-break: break-word; }
|
|
224
|
+
|
|
140
225
|
.request-wrap { position: relative; }
|
|
141
226
|
#request-popover {
|
|
142
227
|
position: absolute;
|
|
@@ -224,9 +309,19 @@ button { font: inherit; cursor: pointer; }
|
|
|
224
309
|
}
|
|
225
310
|
.share-cta:hover { color: var(--text); border-color: #d0d0d5; }
|
|
226
311
|
|
|
227
|
-
.rail-foot { border-top: 1px solid var(--border); padding: 8px 16px; }
|
|
312
|
+
.rail-foot { border-top: 1px solid var(--border); padding: 8px 16px; display: flex; flex-direction: column; gap: 4px; }
|
|
228
313
|
.sync-status { font-size: 11px; color: var(--text-3); display: flex; align-items: center; gap: 6px; }
|
|
229
314
|
.sync-status .dot { width: 6px; height: 6px; }
|
|
315
|
+
.coffee-link {
|
|
316
|
+
display: inline-flex;
|
|
317
|
+
align-items: center;
|
|
318
|
+
gap: 5px;
|
|
319
|
+
font-size: 10.5px;
|
|
320
|
+
color: var(--text-3);
|
|
321
|
+
text-decoration: none;
|
|
322
|
+
width: fit-content;
|
|
323
|
+
}
|
|
324
|
+
.coffee-link:hover { color: var(--text-2); }
|
|
230
325
|
|
|
231
326
|
/* ---------- stage ---------- */
|
|
232
327
|
|
|
@@ -307,12 +402,17 @@ button { font: inherit; cursor: pointer; }
|
|
|
307
402
|
max-width: 100%;
|
|
308
403
|
background: var(--panel);
|
|
309
404
|
transition: max-width 0.18s ease;
|
|
405
|
+
min-height: 100%;
|
|
406
|
+
}
|
|
407
|
+
#frame-wrap.vw-tablet,
|
|
408
|
+
#frame-wrap.vw-mobile {
|
|
409
|
+
margin: 0 auto;
|
|
410
|
+
box-shadow: var(--shadow);
|
|
310
411
|
border-left: 1px solid var(--border);
|
|
311
412
|
border-right: 1px solid var(--border);
|
|
312
|
-
min-height: 100%;
|
|
313
413
|
}
|
|
314
|
-
#frame-wrap.vw-tablet { max-width: 834px;
|
|
315
|
-
#frame-wrap.vw-mobile { max-width: 390px;
|
|
414
|
+
#frame-wrap.vw-tablet { max-width: 834px; }
|
|
415
|
+
#frame-wrap.vw-mobile { max-width: 390px; }
|
|
316
416
|
|
|
317
417
|
#frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: #fff; }
|
|
318
418
|
|
package/src/github.js
CHANGED
|
@@ -3,6 +3,37 @@ import { execFile } from 'node:child_process';
|
|
|
3
3
|
// PR links come from the gh CLI so no tokens are stored or handled: gh uses
|
|
4
4
|
// whoever is already logged in. Machines without gh (or without auth) simply
|
|
5
5
|
// return an empty map and the dashboard shows no PR links.
|
|
6
|
+
// Read only PR detail for the header panel: description plus conversation.
|
|
7
|
+
export function prDetail(repoRoot, number) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
execFile('gh', [
|
|
10
|
+
'pr', 'view', String(number),
|
|
11
|
+
'--json', 'number,title,body,state,author,url,createdAt,comments',
|
|
12
|
+
], { cwd: repoRoot, timeout: 15_000 }, (err, stdout) => {
|
|
13
|
+
if (err) return resolve(null);
|
|
14
|
+
try {
|
|
15
|
+
const d = JSON.parse(stdout);
|
|
16
|
+
resolve({
|
|
17
|
+
number: d.number,
|
|
18
|
+
title: d.title,
|
|
19
|
+
body: d.body || '',
|
|
20
|
+
state: d.state,
|
|
21
|
+
url: d.url,
|
|
22
|
+
author: (d.author && d.author.login) || '',
|
|
23
|
+
createdAt: d.createdAt,
|
|
24
|
+
comments: (d.comments || []).map((c) => ({
|
|
25
|
+
author: (c.author && c.author.login) || '',
|
|
26
|
+
body: c.body || '',
|
|
27
|
+
createdAt: c.createdAt,
|
|
28
|
+
})),
|
|
29
|
+
});
|
|
30
|
+
} catch {
|
|
31
|
+
resolve(null);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
6
37
|
export function listOpenPRs(repoRoot) {
|
|
7
38
|
return new Promise((resolve) => {
|
|
8
39
|
execFile('gh', [
|
package/src/server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { tryGit } from './git.js';
|
|
6
6
|
import { currentBranch } from './detect.js';
|
|
7
7
|
import { serveStatic } from './previews.js';
|
|
8
|
-
import { listOpenPRs } from './github.js';
|
|
8
|
+
import { listOpenPRs, prDetail } from './github.js';
|
|
9
9
|
|
|
10
10
|
const PUBLIC_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'public');
|
|
11
11
|
const SYNC_INTERVAL_MS = 15_000;
|
|
@@ -110,6 +110,19 @@ export class DesignShareServer {
|
|
|
110
110
|
if (req.method === 'GET' && p === '/api/board') {
|
|
111
111
|
return json(res, 200, await this.board());
|
|
112
112
|
}
|
|
113
|
+
if (req.method === 'GET' && p === '/api/pr') {
|
|
114
|
+
const branch = url.searchParams.get('branch') || '';
|
|
115
|
+
const pr = this.prs[branch];
|
|
116
|
+
if (!pr) return json(res, 200, { pr: null });
|
|
117
|
+
this._prDetailCache = this._prDetailCache || new Map();
|
|
118
|
+
const cached = this._prDetailCache.get(pr.number);
|
|
119
|
+
if (cached && Date.now() - cached.at < 120_000) {
|
|
120
|
+
return json(res, 200, { pr: cached.data });
|
|
121
|
+
}
|
|
122
|
+
const data = await prDetail(this.repo.root, pr.number);
|
|
123
|
+
if (data) this._prDetailCache.set(pr.number, { at: Date.now(), data });
|
|
124
|
+
return json(res, 200, { pr: data });
|
|
125
|
+
}
|
|
113
126
|
if (req.method === 'POST' && p === '/api/share') {
|
|
114
127
|
const body = await readBody(req);
|
|
115
128
|
const branch = body.branch || (await currentBranch(this.repo.root));
|