@slowcook-ai/review-overlay 0.6.0 → 0.7.2
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/dist/comment-format.d.ts +6 -1
- package/dist/comment-format.d.ts.map +1 -1
- package/dist/comment-format.js +13 -3
- package/dist/comment-format.js.map +1 -1
- package/dist/github.d.ts +58 -0
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +157 -0
- package/dist/github.js.map +1 -1
- package/dist/react/markdown.d.ts +2 -0
- package/dist/react/markdown.d.ts.map +1 -0
- package/dist/react/markdown.js +123 -0
- package/dist/react/markdown.js.map +1 -0
- package/dist/react/overlay.d.ts +25 -0
- package/dist/react/overlay.d.ts.map +1 -1
- package/dist/react/overlay.js +549 -126
- package/dist/react/overlay.js.map +1 -1
- package/dist/reviewer-session.d.ts +15 -2
- package/dist/reviewer-session.d.ts.map +1 -1
- package/dist/reviewer-session.js +25 -0
- package/dist/reviewer-session.js.map +1 -1
- package/dist/selector.d.ts +0 -8
- package/dist/selector.d.ts.map +1 -1
- package/dist/selector.js +50 -14
- package/dist/selector.js.map +1 -1
- package/package.json +2 -2
package/dist/react/overlay.js
CHANGED
|
@@ -25,8 +25,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
25
25
|
import { useEffect, useState, useRef, useCallback } from "react";
|
|
26
26
|
import { extractSelector, resolveStoredSelector } from "../selector.js";
|
|
27
27
|
import { buildPayload, formatReviewComment, formatLcrIssue, } from "../comment-format.js";
|
|
28
|
-
import { loadPat, savePat, submitComment, createIssue, fetchOverlayComments, loadCachedComments, saveCachedComments, fetchPrLabels, addLabelsToPr, submitPrApproval, APPROVED_LABEL, } from "../github.js";
|
|
29
|
-
import {
|
|
28
|
+
import { loadPat, savePat, submitComment, createIssue, fetchOverlayComments, fetchLcrIssues, loadCachedComments, saveCachedComments, fetchPrLabels, addLabelsToPr, submitPrApproval, fetchDocFile, commitDocFile, APPROVED_LABEL, } from "../github.js";
|
|
29
|
+
import { renderMarkdown } from "./markdown.js";
|
|
30
|
+
import { loadReviewerToken, loadReviewerIdentity, saveReviewerToken, saveReviewerIdentity, clearReviewerSession, runDeviceLogin, identifyReviewer, checkRepoWriteAccess, } from "../reviewer-session.js";
|
|
30
31
|
import { isResolvedStatus } from "../comment-format.js";
|
|
31
32
|
import { readCurrentStory } from "./use-story-marker.js";
|
|
32
33
|
const ACCENT = "#FF6B6B";
|
|
@@ -48,6 +49,21 @@ export function SlowcookReviewOverlay(props) {
|
|
|
48
49
|
const authBase = props.authBase ?? process.env["NEXT_PUBLIC_SLOWCOOK_AUTH_BASE"] ?? "";
|
|
49
50
|
const overlayVersion = props.overlayVersion ?? "0.6.0";
|
|
50
51
|
const repoCoord = { owner, repo };
|
|
52
|
+
// 0.7.0 — docs studio config.
|
|
53
|
+
const docPaths = props.docPaths ??
|
|
54
|
+
(process.env["NEXT_PUBLIC_SLOWCOOK_DOC_PATHS"]?.split(",").map((s) => s.trim()).filter(Boolean)) ??
|
|
55
|
+
["docs/PRD.md", "docs/ROADMAP.md", "docs/USER_STORIES.md", "docs/ARCHITECTURE.md"];
|
|
56
|
+
const branchProp = props.branch ?? process.env["NEXT_PUBLIC_SLOWCOOK_BRANCH"] ?? "";
|
|
57
|
+
// 0.7.1 — review surfaces (persona switcher lives in the pane, not the mock).
|
|
58
|
+
const surfaces = props.surfaces ?? (() => {
|
|
59
|
+
try {
|
|
60
|
+
const raw = process.env["NEXT_PUBLIC_SLOWCOOK_SURFACES"];
|
|
61
|
+
return raw ? JSON.parse(raw) : [];
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
})();
|
|
51
67
|
// 0.5.1 — hydration-mismatch fix. The overlay can't render during
|
|
52
68
|
// SSR (no localStorage, no window.matchMedia, no DOM), so it returns
|
|
53
69
|
// null. But on first client render it would normally render the
|
|
@@ -63,6 +79,8 @@ export function SlowcookReviewOverlay(props) {
|
|
|
63
79
|
// are declared. Bails early during SSR via the typeof window check.
|
|
64
80
|
const [mode, setMode] = useState("nav");
|
|
65
81
|
const [target, setTarget] = useState(null);
|
|
82
|
+
// 0.6.5 — element under the cursor in comment mode (green hover preview).
|
|
83
|
+
const [hoverEl, setHoverEl] = useState(null);
|
|
66
84
|
const [composerOpen, setComposerOpen] = useState(false);
|
|
67
85
|
// 0.5.0 — comments-list panel state. Opened by the "📋" button in
|
|
68
86
|
// the pill OR by clicking the count badge on the Comment toggle.
|
|
@@ -86,6 +104,10 @@ export function SlowcookReviewOverlay(props) {
|
|
|
86
104
|
// comments (applied/declined/noop) hide by default; needs-clarification +
|
|
87
105
|
// unresolved always show.
|
|
88
106
|
const [showApplied, setShowApplied] = useState(false);
|
|
107
|
+
// 0.7.0 — docs studio (textual review): panel open + the resolved working
|
|
108
|
+
// branch a scope-change commits to.
|
|
109
|
+
const [docsPanelOpen, setDocsPanelOpen] = useState(false);
|
|
110
|
+
const [resolvedBranch, setResolvedBranch] = useState(branchProp);
|
|
89
111
|
// 0.2.0 — track viewport width for the icon-only mobile collapse + the
|
|
90
112
|
// picker-route hide. Updates on resize + initial mount.
|
|
91
113
|
const [isMobile, setIsMobile] = useState(false);
|
|
@@ -96,18 +118,107 @@ export function SlowcookReviewOverlay(props) {
|
|
|
96
118
|
// refresh; background-refresh on focus.
|
|
97
119
|
const [comments, setComments] = useState([]);
|
|
98
120
|
const [openCommentId, setOpenCommentId] = useState(null);
|
|
121
|
+
// 0.6.8 — "new activity" badge: track which comment states the reviewer has
|
|
122
|
+
// already seen, so new replies / newly-applied resolutions ping the Comment
|
|
123
|
+
// button with a green count. Signature = id + reply status + reply length.
|
|
124
|
+
const seenKey = `slowcook.review-overlay.seen.${owner}/${repo}`;
|
|
125
|
+
const [seenSigs, setSeenSigs] = useState(() => {
|
|
126
|
+
try {
|
|
127
|
+
return new Set(JSON.parse(window.localStorage.getItem(seenKey) ?? "[]"));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return new Set();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
const sigOf = (r) => `${r.commentId}:${r.plateReply?.status ?? ""}:${(r.plateReply?.summary ?? "").length}`;
|
|
134
|
+
// 0.6.10 — only an AGENT action is an "event": a reply comment (plateCommentUrl)
|
|
135
|
+
// or a resolution (applied). A reviewer's own freshly-filed comment (open issue,
|
|
136
|
+
// no reply) is NOT an event — it shouldn't ping their own badge.
|
|
137
|
+
const isAgentEvent = (r) => r.plateReply != null && (r.plateReply.status === "applied" || !!r.plateCommentUrl);
|
|
138
|
+
const newCount = comments.filter((r) => isAgentEvent(r) && !seenSigs.has(sigOf(r))).length;
|
|
139
|
+
const markAllSeen = useCallback(() => {
|
|
140
|
+
const sigs = comments.map(sigOf);
|
|
141
|
+
setSeenSigs(new Set(sigs));
|
|
142
|
+
try {
|
|
143
|
+
window.localStorage.setItem(seenKey, JSON.stringify(sigs));
|
|
144
|
+
}
|
|
145
|
+
catch { /* ignore */ }
|
|
146
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
|
+
}, [comments]);
|
|
99
148
|
// 0.4.2 — approved state. True when the PR carries the
|
|
100
149
|
// slowcook-mockup-approved label; pill renders green-tinted +
|
|
101
150
|
// hides the Approve button. Nav + Comment still work for follow-up
|
|
102
151
|
// discussion (plate refuses to amend either way).
|
|
103
152
|
const [isApproved, setIsApproved] = useState(false);
|
|
104
|
-
//
|
|
153
|
+
// Lock page scroll while a composer or thread popover is open. Those boxes
|
|
154
|
+
// (and the element highlight) are positioned once at open time, so a scroll
|
|
155
|
+
// would leave them behind — easy to lose on mobile. Freezing the page keeps
|
|
156
|
+
// the box and its highlight together until the reviewer is done.
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
const locked = composerOpen || generalComposerOpen || openCommentId !== null;
|
|
159
|
+
if (!locked || typeof document === "undefined")
|
|
160
|
+
return;
|
|
161
|
+
const body = document.body;
|
|
162
|
+
const prevOverflow = body.style.overflow;
|
|
163
|
+
const prevTouch = body.style.touchAction;
|
|
164
|
+
body.style.overflow = "hidden";
|
|
165
|
+
body.style.touchAction = "none";
|
|
166
|
+
return () => { body.style.overflow = prevOverflow; body.style.touchAction = prevTouch; };
|
|
167
|
+
}, [composerOpen, generalComposerOpen, openCommentId]);
|
|
168
|
+
// 0.7.0 — resolve the working branch for doc scope-changes. Prefer the
|
|
169
|
+
// configured branch; else the head branch of the open mockup PR; else the
|
|
170
|
+
// repo default. Runs once in lcr mode when no branch was provided.
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (typeof window === "undefined" || reviewMode !== "lcr" || branchProp || !owner || !repo)
|
|
173
|
+
return;
|
|
174
|
+
let alive = true;
|
|
175
|
+
(async () => {
|
|
176
|
+
const base = getProxyApiBase() ?? "https://api.github.com";
|
|
177
|
+
try {
|
|
178
|
+
const r = await fetch(`${base}/repos/${owner}/${repo}`, { headers: { Accept: "application/vnd.github+json" } });
|
|
179
|
+
if (r.ok && alive) {
|
|
180
|
+
const j = (await r.json());
|
|
181
|
+
if (j.default_branch)
|
|
182
|
+
setResolvedBranch(j.default_branch);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch { /* offline / rate-limited — Docs read falls back to default ref */ }
|
|
186
|
+
})();
|
|
187
|
+
return () => { alive = false; };
|
|
188
|
+
}, [reviewMode, branchProp, owner, repo]);
|
|
189
|
+
// Mount-time + on-focus fetch of overlay comments / LCR issues.
|
|
105
190
|
useEffect(() => {
|
|
106
191
|
if (typeof window === "undefined")
|
|
107
192
|
return;
|
|
108
|
-
if (!enabled)
|
|
193
|
+
if (!enabled || !owner || !repo)
|
|
109
194
|
return;
|
|
110
|
-
|
|
195
|
+
// 0.6.4 — lcr mode: comments are `lcr-review` ISSUES, retrieved with the
|
|
196
|
+
// signed-in reviewer's token. Closed issues = applied (hidden behind the
|
|
197
|
+
// "show already-applied" toggle); open = shown. Without a token (not signed
|
|
198
|
+
// in yet) there's nothing to read on a private repo — refetches after login
|
|
199
|
+
// via the reviewerIdentity dep.
|
|
200
|
+
if (reviewMode === "lcr") {
|
|
201
|
+
const cached = loadCachedComments(window.localStorage, { owner, repo }, 0);
|
|
202
|
+
if (cached)
|
|
203
|
+
setComments(cached);
|
|
204
|
+
const refresh = () => {
|
|
205
|
+
const token = loadReviewerToken(window.localStorage, { owner, repo });
|
|
206
|
+
if (!token)
|
|
207
|
+
return;
|
|
208
|
+
void fetchLcrIssues({ owner, repo, token })
|
|
209
|
+
.then((records) => {
|
|
210
|
+
setComments(records);
|
|
211
|
+
saveCachedComments(window.localStorage, { owner, repo }, 0, records);
|
|
212
|
+
})
|
|
213
|
+
.catch(() => { });
|
|
214
|
+
};
|
|
215
|
+
refresh();
|
|
216
|
+
const onFocus = () => refresh();
|
|
217
|
+
window.addEventListener("focus", onFocus);
|
|
218
|
+
return () => window.removeEventListener("focus", onFocus);
|
|
219
|
+
}
|
|
220
|
+
// scenarios mode — the pin layer reads PR comments (needs a PR).
|
|
221
|
+
if (!prNumber)
|
|
111
222
|
return;
|
|
112
223
|
const cached = loadCachedComments(window.localStorage, { owner, repo }, prNumber);
|
|
113
224
|
if (cached)
|
|
@@ -133,7 +244,7 @@ export function SlowcookReviewOverlay(props) {
|
|
|
133
244
|
const onFocus = () => refresh();
|
|
134
245
|
window.addEventListener("focus", onFocus);
|
|
135
246
|
return () => window.removeEventListener("focus", onFocus);
|
|
136
|
-
}, [enabled, owner, repo, prNumber]);
|
|
247
|
+
}, [enabled, owner, repo, prNumber, reviewMode, reviewerIdentity]);
|
|
137
248
|
useEffect(() => {
|
|
138
249
|
if (typeof window === "undefined")
|
|
139
250
|
return;
|
|
@@ -174,12 +285,17 @@ export function SlowcookReviewOverlay(props) {
|
|
|
174
285
|
if (!token)
|
|
175
286
|
return;
|
|
176
287
|
try {
|
|
177
|
-
const
|
|
288
|
+
const base = await identifyReviewer(token);
|
|
289
|
+
// Derive the apply tier from repo write access (push/maintain/admin).
|
|
290
|
+
const canApply = await checkRepoWriteAccess(token, { owner, repo });
|
|
291
|
+
const id = { ...base, canApply };
|
|
178
292
|
saveReviewerToken(window.localStorage, { owner, repo }, token);
|
|
179
293
|
saveReviewerIdentity(window.localStorage, { owner, repo }, id);
|
|
180
294
|
setReviewerIdentity(id);
|
|
181
295
|
setLogin({ open: false, status: "" });
|
|
182
|
-
setFeedback(
|
|
296
|
+
setFeedback(canApply
|
|
297
|
+
? `Signed in as @${id.login} — your comments are applied.`
|
|
298
|
+
: `Signed in as @${id.login} — your feedback goes to the team for review.`);
|
|
183
299
|
}
|
|
184
300
|
catch (e) {
|
|
185
301
|
setLogin({ open: true, status: `Could not read your GitHub identity: ${e instanceof Error ? e.message : String(e)}` });
|
|
@@ -197,7 +313,9 @@ export function SlowcookReviewOverlay(props) {
|
|
|
197
313
|
// the `vibe` label, since an LCR note is about a requirement, not a PR.
|
|
198
314
|
const postPayload = useCallback(async (payload, pat) => {
|
|
199
315
|
if (reviewMode === "lcr") {
|
|
200
|
-
|
|
316
|
+
// Only write-access reviewers get the `vibe` (auto-apply) label; others
|
|
317
|
+
// are labelled `community-review` and held for the team to triage.
|
|
318
|
+
const issue = formatLcrIssue({ payload, canApply: reviewerIdentity?.canApply === true });
|
|
201
319
|
const res = await createIssue({
|
|
202
320
|
owner: repoCoord.owner, repo: repoCoord.repo, pat,
|
|
203
321
|
title: issue.title, body: issue.body, labels: issue.labels,
|
|
@@ -211,7 +329,7 @@ export function SlowcookReviewOverlay(props) {
|
|
|
211
329
|
apiBase: getProxyApiBase() ?? undefined,
|
|
212
330
|
});
|
|
213
331
|
return { res, kind: "comment" };
|
|
214
|
-
}, [reviewMode, repoCoord, prNumber]);
|
|
332
|
+
}, [reviewMode, repoCoord, prNumber, reviewerIdentity]);
|
|
215
333
|
useEffect(() => {
|
|
216
334
|
if (typeof window === "undefined")
|
|
217
335
|
return;
|
|
@@ -252,31 +370,75 @@ export function SlowcookReviewOverlay(props) {
|
|
|
252
370
|
useEffect(() => {
|
|
253
371
|
if (typeof window === "undefined")
|
|
254
372
|
return;
|
|
255
|
-
if (mode === "nav")
|
|
373
|
+
if (mode === "nav") {
|
|
374
|
+
setHoverEl(null);
|
|
256
375
|
return;
|
|
376
|
+
}
|
|
377
|
+
const isOwnUi = (el) => !el ||
|
|
378
|
+
(composerRef.current && composerRef.current.contains(el)) ||
|
|
379
|
+
!!el.closest('[data-slowcook-overlay-ui="1"]');
|
|
257
380
|
function onClick(e) {
|
|
258
381
|
const el = e.target;
|
|
259
|
-
if (!el)
|
|
260
|
-
return;
|
|
261
|
-
// Don't capture clicks on the overlay's own UI.
|
|
262
|
-
if (composerRef.current && composerRef.current.contains(el))
|
|
263
|
-
return;
|
|
264
|
-
if (el.closest('[data-slowcook-overlay-ui="1"]'))
|
|
382
|
+
if (!el || isOwnUi(el))
|
|
265
383
|
return;
|
|
266
384
|
e.preventDefault();
|
|
267
385
|
e.stopPropagation();
|
|
268
386
|
if (mode === "comment") {
|
|
269
387
|
setTarget(el);
|
|
388
|
+
setHoverEl(null);
|
|
270
389
|
setComposerOpen(true);
|
|
271
390
|
}
|
|
272
391
|
else if (mode === "approve") {
|
|
273
392
|
void submitApproval();
|
|
274
393
|
}
|
|
275
394
|
}
|
|
395
|
+
// 0.6.5 — live hover preview in comment mode: green-outline the element that
|
|
396
|
+
// would be selected on click, so the reviewer isn't minesweeping. (The red
|
|
397
|
+
// outline marks the element already chosen for the open composer.)
|
|
398
|
+
function onMove(e) {
|
|
399
|
+
if (mode !== "comment")
|
|
400
|
+
return;
|
|
401
|
+
const el = e.target;
|
|
402
|
+
setHoverEl(el && !isOwnUi(el) ? el : null);
|
|
403
|
+
}
|
|
404
|
+
// 0.6.15 — native controls (a <select>, <input>, link, button) activate on
|
|
405
|
+
// mousedown/pointerdown, BEFORE click — so without this they'd open their
|
|
406
|
+
// dropdown / focus / navigate instead of letting the click attach a comment.
|
|
407
|
+
// Swallow the down-press on the page (not the overlay's own UI) in
|
|
408
|
+
// comment/approve mode; the click still fires and opens the composer.
|
|
409
|
+
function onDown(e) {
|
|
410
|
+
const el = e.target;
|
|
411
|
+
if (!el || isOwnUi(el))
|
|
412
|
+
return;
|
|
413
|
+
e.preventDefault();
|
|
414
|
+
e.stopPropagation();
|
|
415
|
+
}
|
|
276
416
|
document.addEventListener("click", onClick, { capture: true });
|
|
277
|
-
|
|
417
|
+
document.addEventListener("mouseover", onMove, { capture: true });
|
|
418
|
+
document.addEventListener("mousedown", onDown, { capture: true });
|
|
419
|
+
document.addEventListener("pointerdown", onDown, { capture: true });
|
|
420
|
+
return () => {
|
|
421
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
422
|
+
document.removeEventListener("mouseover", onMove, { capture: true });
|
|
423
|
+
document.removeEventListener("mousedown", onDown, { capture: true });
|
|
424
|
+
document.removeEventListener("pointerdown", onDown, { capture: true });
|
|
425
|
+
};
|
|
278
426
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
279
427
|
}, [mode]);
|
|
428
|
+
// 0.6.6 — entering Nav clears the whole comment surface: any open composer,
|
|
429
|
+
// the comments-list panel, a general-note composer, an open pin popover, the
|
|
430
|
+
// approve confirm, the hover preview. Nav = "done reviewing, clean slate".
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
if (mode !== "nav")
|
|
433
|
+
return;
|
|
434
|
+
setComposerOpen(false);
|
|
435
|
+
setTarget(null);
|
|
436
|
+
setGeneralComposerOpen(false);
|
|
437
|
+
setListPanelOpen(false);
|
|
438
|
+
setOpenCommentId(null);
|
|
439
|
+
setApproveConfirmOpen(false);
|
|
440
|
+
setHoverEl(null);
|
|
441
|
+
}, [mode]);
|
|
280
442
|
const onApproveClicked = useCallback(() => {
|
|
281
443
|
// 0.2.0 — two-step confirm; protects against fat-finger approval.
|
|
282
444
|
setMode("approve");
|
|
@@ -509,23 +671,45 @@ export function SlowcookReviewOverlay(props) {
|
|
|
509
671
|
if (isPickerRoute)
|
|
510
672
|
return null;
|
|
511
673
|
// 0.5.1 — auto-detect path: skip when env vars + props together
|
|
512
|
-
// don't supply a real owner/repo
|
|
513
|
-
//
|
|
514
|
-
|
|
674
|
+
// don't supply a real owner/repo. Avoids "submit comment" failing
|
|
675
|
+
// silently because the API call goes nowhere.
|
|
676
|
+
// 0.6.1 — lcr mode files ISSUES (not PR comments), so it needs no PR;
|
|
677
|
+
// only scenarios mode requires a prNumber.
|
|
678
|
+
if (!owner || !repo)
|
|
679
|
+
return null;
|
|
680
|
+
if (reviewMode !== "lcr" && !prNumber)
|
|
515
681
|
return null;
|
|
516
682
|
return (_jsxs("div", { "data-slowcook-overlay-ui": "1", style: {
|
|
517
683
|
position: "fixed",
|
|
518
684
|
inset: 0,
|
|
519
685
|
pointerEvents: "none",
|
|
520
686
|
zIndex: 2147483000,
|
|
521
|
-
}, children: [
|
|
687
|
+
}, children: [_jsx("style", { dangerouslySetInnerHTML: { __html: `[data-slowcook-overlay-ui] textarea,[data-slowcook-overlay-ui] input,[data-slowcook-overlay-ui] select{` +
|
|
688
|
+
`color:#1a1a1a !important;background-color:#fff !important;` +
|
|
689
|
+
`-webkit-text-fill-color:#1a1a1a !important;border-color:rgba(0,0,0,0.15) !important;caret-color:#1a1a1a !important;}` +
|
|
690
|
+
`[data-slowcook-overlay-ui] textarea::placeholder,[data-slowcook-overlay-ui] input::placeholder{` +
|
|
691
|
+
`color:rgba(0,0,0,0.4) !important;-webkit-text-fill-color:rgba(0,0,0,0.4) !important;}` +
|
|
692
|
+
// 0.7.2 — the compact pane select. Higher specificity (attr + class +
|
|
693
|
+
// element) so the host's `select{font-size:16px !important}` (iOS-zoom
|
|
694
|
+
// guard) can't bloat it; keep it small + dark to match the pane.
|
|
695
|
+
`[data-slowcook-overlay-ui] select.sc-ovl-pane-select{` +
|
|
696
|
+
`font-size:11px !important;padding:4px 7px !important;line-height:1.15 !important;` +
|
|
697
|
+
`color:#1a1a1a !important;background-color:#fff !important;font-weight:700 !important;}`
|
|
698
|
+
} }), mode !== "nav" && (_jsx("div", { style: {
|
|
522
699
|
position: "absolute",
|
|
523
700
|
inset: 0,
|
|
524
701
|
backgroundColor: mode === "comment"
|
|
525
702
|
? "rgba(255, 107, 107, 0.05)"
|
|
526
703
|
: "rgba(74, 222, 128, 0.05)",
|
|
527
704
|
pointerEvents: "none",
|
|
528
|
-
}, "aria-hidden": "true" })), _jsx(ModeToggle, { mode: mode, onChange: (m) => (m === "approve" ? onApproveClicked() : setMode(m)), disabled: submitting, isMobile: isMobile, isApproved: isApproved, commentCount: comments.length, onListClick: () => setListPanelOpen(true) }
|
|
705
|
+
}, "aria-hidden": "true" })), mode === "comment" && !composerOpen && hoverEl && _jsx(HoverHighlight, { el: hoverEl }), _jsx(ModeToggle, { mode: mode, onChange: (m) => (m === "approve" ? onApproveClicked() : setMode(m)), disabled: submitting, isMobile: isMobile, isApproved: isApproved, commentCount: comments.length, newCount: newCount, onListClick: () => { setListPanelOpen(true); markAllSeen(); }, docsEnabled: reviewMode === "lcr" && docPaths.length > 0, onDocsClick: () => setDocsPanelOpen(true), surfaces: reviewMode === "lcr" ? surfaces : [], onNavigate: (home) => {
|
|
706
|
+
if (typeof window === "undefined")
|
|
707
|
+
return;
|
|
708
|
+
// Router-agnostic SPA nav: pushState + popstate so react-router (or any
|
|
709
|
+
// history listener) updates without a full reload.
|
|
710
|
+
window.history.pushState({}, "", home);
|
|
711
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
712
|
+
}, reviewMode: reviewMode, identity: reviewerIdentity, onSignIn: () => void signIn(), onSignOut: signOut }), mode === "comment" && comments.length > 0 && (_jsx(CommentPins, { records: comments.filter((c) => c.payload.element !== null), showApplied: showApplied, openCommentId: openCommentId, onOpen: (id) => setOpenCommentId(id), onClose: () => setOpenCommentId(null), flashCommentId: flashCommentId })), listPanelOpen && (_jsx(CommentsListPanel, { records: comments, showApplied: showApplied, onToggleApplied: () => setShowApplied((v) => !v), onClose: () => setListPanelOpen(false), onOpenComment: (id) => {
|
|
529
713
|
setListPanelOpen(false);
|
|
530
714
|
setMode("comment");
|
|
531
715
|
setOpenCommentId(id);
|
|
@@ -534,7 +718,7 @@ export function SlowcookReviewOverlay(props) {
|
|
|
534
718
|
}, onAddGeneral: () => {
|
|
535
719
|
setListPanelOpen(false);
|
|
536
720
|
setGeneralComposerOpen(true);
|
|
537
|
-
} })), generalComposerOpen && (_jsx(GeneralComposer, { onCancel: () => setGeneralComposerOpen(false), onSubmit: submitGeneralComment, submitting: submitting })), composerOpen && target && (_jsx(Composer, { target: target, onCancel: () => {
|
|
721
|
+
}, onApprove: () => { setListPanelOpen(false); onApproveClicked(); }, isApproved: isApproved })), docsPanelOpen && (_jsx(DocsPanel, { repo: repoCoord, docPaths: docPaths, branch: resolvedBranch, identity: reviewerIdentity, getToken: () => (typeof window !== "undefined" ? loadReviewerToken(window.localStorage, repoCoord) : null), onSignIn: () => void signIn(), apiBase: getProxyApiBase() ?? undefined, onClose: () => setDocsPanelOpen(false), onFeedback: (t) => setFeedback(t) })), generalComposerOpen && (_jsx(GeneralComposer, { onCancel: () => setGeneralComposerOpen(false), onSubmit: submitGeneralComment, submitting: submitting })), composerOpen && target && (_jsx(Composer, { target: target, onCancel: () => {
|
|
538
722
|
setComposerOpen(false);
|
|
539
723
|
setTarget(null);
|
|
540
724
|
}, onSubmit: submitFromComposer, submitting: submitting, composerRef: composerRef })), approveConfirmOpen && (_jsx(ApproveConfirm, { onCancel: () => {
|
|
@@ -543,21 +727,31 @@ export function SlowcookReviewOverlay(props) {
|
|
|
543
727
|
}, onConfirm: async () => {
|
|
544
728
|
setApproveConfirmOpen(false);
|
|
545
729
|
await submitApproval();
|
|
546
|
-
}, submitting: submitting })),
|
|
730
|
+
}, submitting: submitting })), login.open && (_jsx(ReviewerLoginDialog, { userCode: login.userCode, verificationUri: login.verificationUri, status: login.status, onClose: () => { loginAbort.current = true; setLogin({ open: false, status: "" }); } })), feedback && _jsx(FeedbackToast, { text: feedback, onDismiss: () => setFeedback(null) })] }));
|
|
547
731
|
}
|
|
548
732
|
/**
|
|
549
|
-
|
|
550
|
-
*
|
|
551
|
-
*
|
|
733
|
+
/**
|
|
734
|
+
* 0.6.5 — green hover preview in comment mode. Outlines the element the cursor
|
|
735
|
+
* is over (the one a click would attach the comment to) so the reviewer can see
|
|
736
|
+
* the target before committing — no minesweeping. Distinct from the red outline,
|
|
737
|
+
* which marks the element already chosen for the open composer.
|
|
552
738
|
*/
|
|
553
|
-
function
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
739
|
+
function HoverHighlight({ el }) {
|
|
740
|
+
const r = el.getBoundingClientRect();
|
|
741
|
+
return (_jsx("div", { "data-slowcook-overlay-ui": "1", "aria-hidden": "true", style: {
|
|
742
|
+
position: "fixed",
|
|
743
|
+
top: r.top, left: r.left, width: r.width, height: r.height,
|
|
744
|
+
border: "2px solid #22c55e",
|
|
745
|
+
background: "rgba(34, 197, 94, 0.12)",
|
|
746
|
+
borderRadius: 4,
|
|
747
|
+
boxShadow: "0 0 0 1px rgba(34,197,94,0.35)",
|
|
748
|
+
pointerEvents: "none",
|
|
749
|
+
zIndex: 2147483200,
|
|
750
|
+
} }));
|
|
557
751
|
}
|
|
558
752
|
/** 0.6.0 — device-flow login dialog: shows the user code + verification link. */
|
|
559
753
|
function ReviewerLoginDialog({ userCode, verificationUri, status, onClose, }) {
|
|
560
|
-
return (_jsx("div", { style: { position: "fixed", inset: 0, background: "rgba(0,0,0,0.45)", display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "auto", zIndex:
|
|
754
|
+
return (_jsx("div", { style: { position: "fixed", inset: 0, background: "rgba(0,0,0,0.45)", display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "auto", zIndex: 2147483647 /* 0.7.1 — above the Docs panel (…602) so the sign-in popup isn't hidden behind it */ }, children: _jsxs("div", { style: { background: "#fff", borderRadius: 14, padding: 24, width: 360, maxWidth: "90vw", boxShadow: "0 12px 40px rgba(0,0,0,0.3)", fontFamily: "system-ui, sans-serif" }, children: [_jsx("div", { style: { fontSize: 17, fontWeight: 700, marginBottom: 6, color: "#111" }, children: "Sign in with GitHub" }), userCode ? (_jsxs(_Fragment, { children: [_jsx("p", { style: { fontSize: 13.5, color: "#555", margin: "0 0 14px" }, children: "Open GitHub and enter this code to review as yourself:" }), _jsx("div", { style: { fontSize: 30, fontWeight: 800, letterSpacing: "0.18em", textAlign: "center", background: "#f3f4f6", borderRadius: 10, padding: "12px 0", color: "#111", fontFamily: "ui-monospace, monospace" }, children: userCode }), _jsxs("a", { href: verificationUri, target: "_blank", rel: "noreferrer", style: { display: "block", textAlign: "center", marginTop: 14, background: "#2da44e", color: "#fff", borderRadius: 999, padding: "9px 0", textDecoration: "none", fontWeight: 600, fontSize: 14 }, children: ["Open ", verificationUri?.replace(/^https?:\/\//, "")] })] })) : (_jsx("p", { style: { fontSize: 14, color: "#555", margin: "8px 0 14px" }, children: status || "Starting…" })), userCode && _jsx("p", { style: { fontSize: 12, color: "#888", marginTop: 12 }, children: status }), _jsx("button", { onClick: onClose, style: { marginTop: 16, width: "100%", border: "1px solid rgba(0,0,0,0.12)", background: "#fff", borderRadius: 999, padding: "8px 0", cursor: "pointer", fontSize: 13, color: "#444" }, children: "Cancel" })] }) }));
|
|
561
755
|
}
|
|
562
756
|
/**
|
|
563
757
|
* 0.2.0 — draggable toggle pill with grip handle, slowcook logo,
|
|
@@ -596,16 +790,31 @@ function saveTogglePosition(p) {
|
|
|
596
790
|
}
|
|
597
791
|
}
|
|
598
792
|
function ModeToggle(props) {
|
|
599
|
-
const { mode, onChange, disabled, isMobile, isApproved, commentCount, onListClick } = props;
|
|
793
|
+
const { mode, onChange, disabled, isMobile, isApproved, commentCount, newCount, onListClick, docsEnabled, onDocsClick, surfaces, onNavigate, reviewMode, identity, onSignIn, onSignOut } = props;
|
|
600
794
|
// 0.5.1 — initialise with the default; load saved position from
|
|
601
795
|
// localStorage AFTER mount. Eliminates a hydration mismatch where
|
|
602
796
|
// SSR/first-client render disagreed on the position value.
|
|
603
797
|
const [pos, setPos] = useState({ top: 12, right: 12 });
|
|
604
798
|
useEffect(() => { setPos(loadTogglePosition()); }, []);
|
|
605
799
|
const dragRef = useRef(null);
|
|
800
|
+
// 0.6.3 — sign-out is a two-step confirm: the disk floats, so a single
|
|
801
|
+
// mis-click shouldn't log you out. First click/tap on the identity chip arms
|
|
802
|
+
// confirmation (the chip turns into "Sign out?"); a second click/tap within a
|
|
803
|
+
// few seconds confirms. ANY other action (mode toggle, list, drag) cancels.
|
|
804
|
+
const [confirmLogout, setConfirmLogout] = useState(false);
|
|
805
|
+
const cancelLogout = useCallback(() => setConfirmLogout(false), []);
|
|
806
|
+
useEffect(() => {
|
|
807
|
+
if (!confirmLogout)
|
|
808
|
+
return;
|
|
809
|
+
const t = setTimeout(() => setConfirmLogout(false), 3500);
|
|
810
|
+
return () => clearTimeout(t);
|
|
811
|
+
}, [confirmLogout]);
|
|
812
|
+
const onChangeSafe = useCallback((m) => { setConfirmLogout(false); onChange(m); }, [onChange]);
|
|
813
|
+
const onListSafe = useCallback(() => { setConfirmLogout(false); onListClick(); }, [onListClick]);
|
|
606
814
|
// Drag handlers — pointer events for unified mouse + touch.
|
|
607
815
|
const onPointerDown = useCallback((e) => {
|
|
608
816
|
// Only drag from the grip itself; clicks on toggle buttons must stay clicks.
|
|
817
|
+
setConfirmLogout(false); // dragging cancels a pending sign-out confirm
|
|
609
818
|
e.preventDefault();
|
|
610
819
|
e.stopPropagation();
|
|
611
820
|
const target = e.currentTarget;
|
|
@@ -641,13 +850,22 @@ function ModeToggle(props) {
|
|
|
641
850
|
pointerEvents: "auto",
|
|
642
851
|
display: "flex",
|
|
643
852
|
alignItems: "center",
|
|
853
|
+
// 0.7.1 — wrap to multiple rows when crowded (Nav/Comment + 📋 + Docs +
|
|
854
|
+
// persona switcher + sign-in don't fit one row on portrait mobile).
|
|
855
|
+
flexWrap: "wrap",
|
|
856
|
+
justifyContent: "flex-end",
|
|
857
|
+
// 0.7.2 — cap to the viewport (not a fixed px) so it stays one row on
|
|
858
|
+
// desktop but wraps to two on portrait mobile when crowded (Comment mode
|
|
859
|
+
// adds 📋 + Sign-in on top of Docs + the persona switcher).
|
|
860
|
+
maxWidth: "94vw",
|
|
644
861
|
gap: 4,
|
|
862
|
+
rowGap: 5,
|
|
645
863
|
// 0.4.2 — green-tinted background + green border when approved.
|
|
646
864
|
background: isApproved
|
|
647
865
|
? "rgba(20, 83, 45, 0.92)" // dark-green pill
|
|
648
866
|
: "rgba(15, 15, 24, 0.92)", // default dark
|
|
649
|
-
padding: "
|
|
650
|
-
borderRadius:
|
|
867
|
+
padding: "5px 6px",
|
|
868
|
+
borderRadius: 16,
|
|
651
869
|
border: isApproved
|
|
652
870
|
? `1px solid rgba(34, 197, 94, 0.55)` // brighter green border
|
|
653
871
|
: "1px solid rgba(255, 255, 255, 0.16)",
|
|
@@ -668,17 +886,15 @@ function ModeToggle(props) {
|
|
|
668
886
|
justifyContent: "center",
|
|
669
887
|
opacity: 0.55,
|
|
670
888
|
touchAction: "none",
|
|
671
|
-
}, children: _jsxs("svg", { width: "6", height: "14", viewBox: "0 0 6 14", "aria-hidden": "true", children: [_jsx("circle", { cx: "1.5", cy: "2", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "4.5", cy: "2", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "1.5", cy: "7", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "4.5", cy: "7", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "1.5", cy: "12", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "4.5", cy: "12", r: "1.1", fill: "currentColor" })] }) }), _jsx(ToggleButton, { active: mode === "
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
background: APPROVED_GREEN,
|
|
678
|
-
color: "white",
|
|
679
|
-
|
|
680
|
-
fontSize: 13,
|
|
681
|
-
}, children: "\u2713 Approved" })) : (_jsx(ToggleButton, { active: mode === "approve", onClick: () => onChange("approve"), disabled: disabled, label: isMobile ? "✅" : "✅ Approve", title: "Approve the mockup (asks for confirmation)", approve: true })), _jsxs("button", { type: "button", onClick: onListClick, disabled: disabled, title: `See all comments (${commentCount})`, style: {
|
|
889
|
+
}, children: _jsxs("svg", { width: "6", height: "14", viewBox: "0 0 6 14", "aria-hidden": "true", children: [_jsx("circle", { cx: "1.5", cy: "2", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "4.5", cy: "2", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "1.5", cy: "7", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "4.5", cy: "7", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "1.5", cy: "12", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "4.5", cy: "12", r: "1.1", fill: "currentColor" })] }) }), _jsx(ToggleButton, { active: mode === "comment", onClick: () => onChangeSafe(mode === "comment" ? "nav" : "comment"), disabled: disabled, label: mode === "comment"
|
|
890
|
+
? (isMobile ? "💬" : "💬 Commenting")
|
|
891
|
+
: (isMobile ? "🧭" : "🧭 Navigating"), title: mode === "comment"
|
|
892
|
+
? "Comment mode on — click to go back to navigating"
|
|
893
|
+
: (newCount ? `Click to comment — ${newCount} new update(s)` : "Click to comment on an element"), accent: true, badge: newCount }), isApproved && (_jsx("span", { "data-slowcook-overlay-ui": "1", title: "Mockup approved \u2014 comment thread stays open for follow-up; plate refuses to amend", style: {
|
|
894
|
+
display: "inline-flex", alignItems: "center", gap: 4,
|
|
895
|
+
padding: "6px 12px", borderRadius: 999, background: APPROVED_GREEN,
|
|
896
|
+
color: "white", fontWeight: 700, fontSize: 13,
|
|
897
|
+
}, children: "\u2713 Approved" })), mode === "comment" && (_jsxs("button", { type: "button", onClick: onListSafe, disabled: disabled, title: `See all comments (${commentCount})`, style: {
|
|
682
898
|
marginLeft: 4,
|
|
683
899
|
background: "rgba(255,255,255,0.06)",
|
|
684
900
|
color: "white",
|
|
@@ -701,7 +917,40 @@ function ModeToggle(props) {
|
|
|
701
917
|
fontWeight: 700,
|
|
702
918
|
minWidth: 14,
|
|
703
919
|
textAlign: "center",
|
|
704
|
-
}, children: commentCount }))] })
|
|
920
|
+
}, children: commentCount }))] })), docsEnabled && (_jsx("button", { type: "button", onClick: () => { setConfirmLogout(false); onDocsClick(); }, disabled: disabled, title: "Review & edit the spec docs (textual review)", style: {
|
|
921
|
+
marginLeft: 4, background: "rgba(255,255,255,0.06)", color: "white",
|
|
922
|
+
border: "none", padding: "6px 10px", borderRadius: 999,
|
|
923
|
+
cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? 0.6 : 1,
|
|
924
|
+
font: "inherit", fontSize: 12, display: "inline-flex", alignItems: "center", gap: 5,
|
|
925
|
+
}, children: "\uD83D\uDCC4 Docs" })), surfaces.length > 0 && _jsx(SurfaceSwitcher, { surfaces: surfaces, onNavigate: onNavigate, disabled: disabled }), reviewMode === "lcr" && mode === "comment" && (identity ? (_jsx("span", { title: confirmLogout
|
|
926
|
+
? "Click again to sign out (or click anything else to cancel)"
|
|
927
|
+
: identity.canApply
|
|
928
|
+
? `Signed in as @${identity.login} — you have write access, so your comments are applied`
|
|
929
|
+
: `Signed in as @${identity.login} — no write access, so your feedback is gathered for the team to review (not auto-applied)`, onClick: () => { if (confirmLogout) {
|
|
930
|
+
setConfirmLogout(false);
|
|
931
|
+
onSignOut();
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
setConfirmLogout(true);
|
|
935
|
+
} }, style: {
|
|
936
|
+
marginLeft: 4, display: "inline-flex", alignItems: "center", gap: 6,
|
|
937
|
+
padding: "4px 10px 4px 4px", borderRadius: 999, cursor: "pointer",
|
|
938
|
+
background: confirmLogout ? "rgba(255,107,107,0.25)" : "rgba(255,255,255,0.10)",
|
|
939
|
+
border: confirmLogout ? "1px solid rgba(255,107,107,0.7)" : "1px solid transparent",
|
|
940
|
+
color: "white", fontSize: 12, fontWeight: 600,
|
|
941
|
+
}, children: confirmLogout ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\uD83D\uDEAA" }), " Sign out?"] })) : (_jsxs(_Fragment, { children: [identity.avatarUrl
|
|
942
|
+
? _jsx("img", { src: identity.avatarUrl, alt: "", width: 20, height: 20, style: { borderRadius: "50%" } })
|
|
943
|
+
: _jsx("span", { "aria-hidden": true, style: { width: 20, height: 20, borderRadius: "50%", background: "rgba(255,255,255,0.2)", display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: 11 }, children: "\uD83D\uDC64" }), "@", identity.login, _jsx("span", { style: {
|
|
944
|
+
fontSize: 9.5, fontWeight: 800, letterSpacing: 0.3, textTransform: "uppercase",
|
|
945
|
+
padding: "1px 6px", borderRadius: 999,
|
|
946
|
+
background: identity.canApply ? "rgba(34,197,94,0.22)" : "rgba(148,163,184,0.25)",
|
|
947
|
+
color: identity.canApply ? "#4ade80" : "#cbd5e1",
|
|
948
|
+
}, children: identity.canApply ? "applies" : "review" })] })) })) : (_jsxs("button", { type: "button", onClick: onSignIn, disabled: disabled, title: "Sign in with GitHub to comment as yourself", style: {
|
|
949
|
+
marginLeft: 4, display: "inline-flex", alignItems: "center", gap: 6,
|
|
950
|
+
padding: "6px 12px", borderRadius: 999, border: "none",
|
|
951
|
+
background: "white", color: "#24292f", cursor: disabled ? "not-allowed" : "pointer",
|
|
952
|
+
font: "inherit", fontSize: 12, fontWeight: 700,
|
|
953
|
+
}, children: [_jsx("svg", { width: "13", height: "13", viewBox: "0 0 16 16", fill: "#24292f", "aria-hidden": "true", children: _jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" }) }), "Sign in"] })))] }));
|
|
705
954
|
}
|
|
706
955
|
/**
|
|
707
956
|
* Two-step approval confirm — guards against fat-finger taps on the
|
|
@@ -759,7 +1008,8 @@ function ToggleButton(props) {
|
|
|
759
1008
|
? ACCENT
|
|
760
1009
|
: "rgba(255,255,255,0.18)"
|
|
761
1010
|
: "transparent";
|
|
762
|
-
return (
|
|
1011
|
+
return (_jsxs("button", { type: "button", onClick: props.onClick, disabled: props.disabled, title: props.title, style: {
|
|
1012
|
+
position: "relative",
|
|
763
1013
|
background: bg,
|
|
764
1014
|
color: "white",
|
|
765
1015
|
border: "none",
|
|
@@ -768,7 +1018,28 @@ function ToggleButton(props) {
|
|
|
768
1018
|
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
769
1019
|
opacity: props.disabled ? 0.6 : 1,
|
|
770
1020
|
font: "inherit",
|
|
771
|
-
}, children: props.label
|
|
1021
|
+
}, children: [props.label, props.badge ? (_jsx("span", { style: {
|
|
1022
|
+
position: "absolute", top: -5, right: -5, minWidth: 16, height: 16,
|
|
1023
|
+
padding: "0 4px", borderRadius: 999, background: "#22c55e", color: "white",
|
|
1024
|
+
fontSize: 10, fontWeight: 800, display: "inline-flex", alignItems: "center",
|
|
1025
|
+
justifyContent: "center", boxShadow: "0 0 0 2px rgba(15,15,24,0.92)",
|
|
1026
|
+
}, children: props.badge })) : null] }));
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* 0.6.13 — page-context badge in the composer so the reviewer sees which page
|
|
1030
|
+
* the comment is on (route + the story it declares, if any) before submitting.
|
|
1031
|
+
*/
|
|
1032
|
+
function PageBadge() {
|
|
1033
|
+
if (typeof window === "undefined")
|
|
1034
|
+
return null;
|
|
1035
|
+
const route = window.location.pathname + window.location.search;
|
|
1036
|
+
const story = readCurrentStory();
|
|
1037
|
+
return (_jsxs("div", { style: {
|
|
1038
|
+
display: "inline-flex", alignItems: "center", gap: 6, maxWidth: "100%",
|
|
1039
|
+
fontSize: 11.5, color: "#3a3a3a", background: "rgba(255,107,107,0.10)",
|
|
1040
|
+
border: "1px solid rgba(255,107,107,0.30)", borderRadius: 6,
|
|
1041
|
+
padding: "3px 9px", marginBottom: 8,
|
|
1042
|
+
}, children: [_jsx("span", { "aria-hidden": true, children: "\uD83D\uDCC4" }), _jsx("span", { style: { fontFamily: "ui-monospace, monospace", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: route }), story && _jsxs("span", { style: { fontWeight: 700, color: "#d6336c" }, children: ["\u00B7 story-", story] })] }));
|
|
772
1043
|
}
|
|
773
1044
|
function Composer(props) {
|
|
774
1045
|
const [prose, setProse] = useState("");
|
|
@@ -830,12 +1101,13 @@ function Composer(props) {
|
|
|
830
1101
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
831
1102
|
fontSize: 13,
|
|
832
1103
|
zIndex: 2147483647,
|
|
833
|
-
}, children: [_jsx("div", { style: { fontWeight: 600, marginBottom:
|
|
1104
|
+
}, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 6 }, children: "Review comment" }), _jsx(PageBadge, {}), _jsx("div", { style: { fontFamily: "ui-monospace, SFMono-Regular, monospace", fontSize: 11, opacity: 0.7, marginBottom: 8, wordBreak: "break-all" }, children: sel.selector }), _jsx("textarea", { "aria-label": "Comment text", autoFocus: true, value: prose, onChange: (e) => setProse(e.target.value), placeholder: "What's off about this element?", rows: 5, style: {
|
|
834
1105
|
width: "100%",
|
|
835
1106
|
padding: 8,
|
|
836
1107
|
border: "1px solid rgba(0,0,0,0.15)",
|
|
837
1108
|
borderRadius: 6,
|
|
838
1109
|
font: "inherit",
|
|
1110
|
+
fontSize: 16, // 0.6.12 — ≥16px stops iOS Safari auto-zooming on focus
|
|
839
1111
|
resize: "vertical",
|
|
840
1112
|
boxSizing: "border-box",
|
|
841
1113
|
} }), _jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 12 }, children: [_jsx("button", { type: "button", onClick: props.onCancel, disabled: props.submitting, style: {
|
|
@@ -941,7 +1213,13 @@ function ensurePat(repo, lcr = false) {
|
|
|
941
1213
|
* sticky to the underlying DOM.
|
|
942
1214
|
*/
|
|
943
1215
|
function CommentPins(props) {
|
|
944
|
-
const { records, openCommentId, onOpen, onClose, flashCommentId } = props;
|
|
1216
|
+
const { records: allRecords, showApplied, openCommentId, onOpen, onClose, flashCommentId } = props;
|
|
1217
|
+
// Hide pins for resolved/applied comments unless the reviewer has asked to
|
|
1218
|
+
// see them (the same "show applied" toggle that governs the list). An open
|
|
1219
|
+
// pin stays visible so "Locate from list" still works.
|
|
1220
|
+
const records = allRecords.filter((r) => showApplied ||
|
|
1221
|
+
r.commentId === openCommentId ||
|
|
1222
|
+
!(r.plateReply != null && isResolvedStatus(r.plateReply.status)));
|
|
945
1223
|
// tick forces a re-render on every animation frame so pins follow
|
|
946
1224
|
// the underlying DOM as the page scrolls / reflows.
|
|
947
1225
|
const [tick, setTick] = useState(0);
|
|
@@ -995,7 +1273,9 @@ function CommentPins(props) {
|
|
|
995
1273
|
}
|
|
996
1274
|
return { record: r, rect, drifted };
|
|
997
1275
|
});
|
|
998
|
-
return (_jsxs("div", { "data-slowcook-overlay-ui": "1", style: { position: "absolute", inset: 0, pointerEvents: "none" }, children: [placements
|
|
1276
|
+
return (_jsxs("div", { "data-slowcook-overlay-ui": "1", style: { position: "absolute", inset: 0, pointerEvents: "none" }, children: [placements
|
|
1277
|
+
.filter(({ record, drifted }) => !drifted || record.commentId === openCommentId)
|
|
1278
|
+
.map(({ record, rect, drifted }) => (_jsx(PinIcon, { record: record, x: rect.x, y: rect.y, drifted: drifted, flashing: flashCommentId === record.commentId, onClick: () => onOpen(record.commentId) }, record.commentId))), openCommentId !== null && (() => {
|
|
999
1279
|
const placement = placements.find((p) => p.record.commentId === openCommentId);
|
|
1000
1280
|
if (!placement)
|
|
1001
1281
|
return null;
|
|
@@ -1010,41 +1290,70 @@ function pinPalette(status, drifted) {
|
|
|
1010
1290
|
case "declined": return { bg: "#94a3b8", fg: "white", glyph: "⊘", ring: "rgba(148, 163, 184, 0.35)" };
|
|
1011
1291
|
case "spec-altering": return { bg: "#facc15", fg: "#1a1a1a", glyph: "!", ring: "rgba(250, 204, 21, 0.35)" };
|
|
1012
1292
|
case "noop": return { bg: "#94a3b8", fg: "white", glyph: "•", ring: "rgba(148, 163, 184, 0.35)" };
|
|
1293
|
+
case "needs-clarification": return { bg: "#4D96FF", fg: "white", glyph: "?", ring: "rgba(77, 150, 255, 0.35)" };
|
|
1013
1294
|
default: return { bg: ACCENT, fg: "white", glyph: "💬", ring: "rgba(255, 107, 107, 0.35)" };
|
|
1014
1295
|
}
|
|
1015
1296
|
}
|
|
1297
|
+
/** Per-author colour — Figma-style. Hashes the FULL author handle to a hue, so
|
|
1298
|
+
* two reviewers with the same initial still get distinct colours. */
|
|
1299
|
+
function authorColor(author) {
|
|
1300
|
+
let h = 0;
|
|
1301
|
+
for (let i = 0; i < author.length; i++)
|
|
1302
|
+
h = (h * 31 + author.charCodeAt(i)) >>> 0;
|
|
1303
|
+
const hue = h % 360;
|
|
1304
|
+
return { bg: `hsl(${hue}, 58%, 42%)`, ring: `hsla(${hue}, 58%, 42%, 0.35)` };
|
|
1305
|
+
}
|
|
1306
|
+
/** First alphanumeric character of the author handle, uppercased. */
|
|
1307
|
+
function authorInitial(author) {
|
|
1308
|
+
const m = author.replace(/[^a-zA-Z0-9]/g, "");
|
|
1309
|
+
return (m[0] ?? "?").toUpperCase();
|
|
1310
|
+
}
|
|
1016
1311
|
function PinIcon(props) {
|
|
1312
|
+
const author = props.record.author || "unknown";
|
|
1017
1313
|
const status = props.record.plateReply?.status ?? null;
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1314
|
+
const col = authorColor(author);
|
|
1315
|
+
// Status is shown as a small corner badge (not the whole pin) so the pin's
|
|
1316
|
+
// body can carry the author's identity instead. No badge for a plain
|
|
1317
|
+
// unresolved comment; drifted/resolved states get one.
|
|
1318
|
+
const statusBadge = props.drifted
|
|
1319
|
+
? pinPalette(null, true)
|
|
1320
|
+
: status
|
|
1321
|
+
? pinPalette(status, false)
|
|
1322
|
+
: null;
|
|
1323
|
+
const title = `@${author}${props.drifted ? " · selector drifted" : status ? ` · ${status}` : ""} · click to view`;
|
|
1324
|
+
return (_jsxs("button", { type: "button", "data-slowcook-overlay-ui": "1", "data-slowcook-comment-id": props.record.commentId, title: title, onClick: (e) => { e.stopPropagation(); props.onClick(); }, style: {
|
|
1023
1325
|
position: "absolute",
|
|
1024
1326
|
left: props.x,
|
|
1025
1327
|
top: props.y,
|
|
1026
1328
|
width: 22,
|
|
1027
1329
|
height: 22,
|
|
1028
1330
|
borderRadius: 999,
|
|
1029
|
-
background:
|
|
1030
|
-
color:
|
|
1331
|
+
background: col.bg,
|
|
1332
|
+
color: "#fff",
|
|
1031
1333
|
border: `2px solid white`,
|
|
1032
1334
|
boxShadow: props.flashing
|
|
1033
|
-
? `0 2px 6px rgba(0,0,0,0.25), 0 0 0 12px ${
|
|
1034
|
-
: `0 2px 6px rgba(0,0,0,0.25), 0 0 0 4px ${
|
|
1335
|
+
? `0 2px 6px rgba(0,0,0,0.25), 0 0 0 12px ${col.ring}`
|
|
1336
|
+
: `0 2px 6px rgba(0,0,0,0.25), 0 0 0 4px ${col.ring}`,
|
|
1035
1337
|
transform: props.flashing ? "scale(1.4)" : "scale(1)",
|
|
1036
1338
|
transition: "transform 220ms ease, box-shadow 220ms ease",
|
|
1037
1339
|
cursor: "pointer",
|
|
1038
1340
|
pointerEvents: "auto",
|
|
1039
1341
|
fontSize: 11,
|
|
1040
|
-
fontWeight:
|
|
1342
|
+
fontWeight: 800,
|
|
1041
1343
|
display: "flex",
|
|
1042
1344
|
alignItems: "center",
|
|
1043
1345
|
justifyContent: "center",
|
|
1044
1346
|
font: "inherit",
|
|
1045
1347
|
padding: 0,
|
|
1046
1348
|
lineHeight: 1,
|
|
1047
|
-
}, children:
|
|
1349
|
+
}, children: [authorInitial(author), statusBadge && (_jsx("span", { "aria-hidden": true, style: {
|
|
1350
|
+
position: "absolute", top: -5, right: -5,
|
|
1351
|
+
width: 12, height: 12, borderRadius: 999,
|
|
1352
|
+
background: statusBadge.bg, color: statusBadge.fg,
|
|
1353
|
+
border: "1.5px solid white", fontSize: 8, fontWeight: 800,
|
|
1354
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
1355
|
+
lineHeight: 1,
|
|
1356
|
+
}, children: statusBadge.glyph }))] }));
|
|
1048
1357
|
}
|
|
1049
1358
|
/**
|
|
1050
1359
|
* Thread popover — opens above the pin (or below when near the
|
|
@@ -1114,7 +1423,7 @@ function CommentThreadPopover(props) {
|
|
|
1114
1423
|
background: "rgba(34, 197, 94, 0.08)",
|
|
1115
1424
|
borderLeft: "3px solid #22c55e",
|
|
1116
1425
|
borderRadius: 4,
|
|
1117
|
-
}, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 600, color: "#15803d", marginBottom: 4 }, children: "slowcook plate" }), _jsx("div", { style: { whiteSpace: "pre-wrap" }, children: record.plateReply.summary }), record.plateReply.files_touched && record.plateReply.files_touched.length > 0 && (_jsxs("div", { style: { marginTop: 6, fontSize: 11, opacity: 0.7, fontFamily: "ui-monospace, SFMono-Regular, monospace" }, children: ["touched: ", record.plateReply.files_touched.join(", ")] }))] })), _jsxs("div", { style: { display: "flex", gap: 12, marginTop: 12, fontSize: 11 }, children: [_jsx("a", { href: record.htmlUrl, target: "_blank", rel: "noreferrer", style: { color: ACCENT, textDecoration: "none" }, children: "\u2197 Comment on GitHub" }), record.plateCommentUrl && (_jsx("a", { href: record.plateCommentUrl, target: "_blank", rel: "noreferrer", style: { color: "#22c55e", textDecoration: "none" }, children: "\u2197
|
|
1426
|
+
}, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 600, color: "#15803d", marginBottom: 4 }, children: "slowcook plate" }), _jsx("div", { style: { whiteSpace: "pre-wrap" }, children: record.plateReply.summary }), record.plateReply.files_touched && record.plateReply.files_touched.length > 0 && (_jsxs("div", { style: { marginTop: 6, fontSize: 11, opacity: 0.7, fontFamily: "ui-monospace, SFMono-Regular, monospace" }, children: ["touched: ", record.plateReply.files_touched.join(", ")] }))] })), _jsxs("div", { style: { display: "flex", gap: 12, marginTop: 12, fontSize: 11 }, children: [_jsx("a", { href: record.htmlUrl, target: "_blank", rel: "noreferrer", style: { color: ACCENT, textDecoration: "none" }, children: "\u2197 Comment on GitHub" }), record.plateCommentUrl && (_jsx("a", { href: record.plateCommentUrl, target: "_blank", rel: "noreferrer", style: { color: "#22c55e", textDecoration: "none" }, children: "\u2197 Reply on GitHub" }))] })] }));
|
|
1118
1427
|
}
|
|
1119
1428
|
function formatTimeAgo(iso) {
|
|
1120
1429
|
try {
|
|
@@ -1146,7 +1455,10 @@ function formatTimeAgo(iso) {
|
|
|
1146
1455
|
* + visible, also flashes the pin in-place.
|
|
1147
1456
|
*/
|
|
1148
1457
|
function CommentsListPanel(props) {
|
|
1149
|
-
const { records, showApplied, onToggleApplied, onClose, onOpenComment, onAddGeneral } = props;
|
|
1458
|
+
const { records, showApplied, onToggleApplied, onClose, onOpenComment, onAddGeneral, onApprove, isApproved } = props;
|
|
1459
|
+
// 0.6.14 — expand/collapse a row in place to read the full prose + reply,
|
|
1460
|
+
// open the GitHub issue, or (for anchored comments) locate the pin on the page.
|
|
1461
|
+
const [expandedId, setExpandedId] = useState(null);
|
|
1150
1462
|
// 0.6.0 — resolved comments (applied/declined/noop) hide by default so the
|
|
1151
1463
|
// list shows what still needs attention; needs-clarification + unresolved
|
|
1152
1464
|
// always show. The toggle reveals the resolved ones.
|
|
@@ -1178,69 +1490,62 @@ function CommentsListPanel(props) {
|
|
|
1178
1490
|
fontSize: 18,
|
|
1179
1491
|
lineHeight: 1,
|
|
1180
1492
|
padding: 0,
|
|
1181
|
-
}, children: "\u00D7" })] }),
|
|
1182
|
-
width: "100%",
|
|
1183
|
-
padding: "10px 12px",
|
|
1184
|
-
background: "rgba(255,107,107,0.12)",
|
|
1185
|
-
color: ACCENT,
|
|
1186
|
-
border: `1px dashed ${ACCENT}`,
|
|
1187
|
-
borderRadius: 8,
|
|
1188
|
-
cursor: "pointer",
|
|
1189
|
-
font: "inherit",
|
|
1190
|
-
fontWeight: 600,
|
|
1191
|
-
fontSize: 13,
|
|
1192
|
-
}, children: "+ Add note (about the page, not an element)" }) }), hiddenCount > 0 && (_jsx("button", { type: "button", onClick: onToggleApplied, style: { margin: "0 12px 8px", padding: "7px 10px", background: "transparent", color: "rgba(255,255,255,0.6)", border: "1px solid rgba(255,255,255,0.12)", borderRadius: 8, cursor: "pointer", font: "inherit", fontSize: 12 }, children: showApplied ? `Hide ${hiddenCount} already-applied` : `Show ${hiddenCount} already-applied` })), _jsx("div", { style: { overflow: "auto", flex: 1, padding: 8 }, children: visible.length === 0 ? (_jsx("div", { style: { padding: 16, opacity: 0.55, textAlign: "center", fontSize: 12 }, children: records.length === 0
|
|
1193
|
-
? "No comments yet. Toggle 💬 Comment + click an element to anchor a comment, or use the button above for a page-level note."
|
|
1194
|
-
: "Nothing needs attention — all comments are applied. Use the toggle above to see them." })) : (visible.slice().reverse().map((r) => {
|
|
1195
|
-
const status = r.plateReply?.status ?? null;
|
|
1196
|
-
const palette = pinPalette(status, false);
|
|
1197
|
-
const anchored = r.payload.element !== null;
|
|
1198
|
-
const live = anchored && typeof document !== "undefined"
|
|
1199
|
-
? resolveStoredSelector(document, r.payload.element.selector, r.payload.element.fallback_selector)
|
|
1200
|
-
: null;
|
|
1201
|
-
const hidden = anchored && live && (live.element.getBoundingClientRect().width === 0 ||
|
|
1202
|
-
live.element.offsetParent === null);
|
|
1203
|
-
const anchorLabel = !anchored
|
|
1204
|
-
? { text: "note", color: "#94a3b8", bg: "rgba(148,163,184,0.18)" }
|
|
1205
|
-
: !live
|
|
1206
|
-
? { text: "drifted", color: "#facc15", bg: "rgba(250,204,21,0.18)" }
|
|
1207
|
-
: hidden
|
|
1208
|
-
? { text: "hidden", color: "#94a3b8", bg: "rgba(148,163,184,0.18)" }
|
|
1209
|
-
: { text: "anchored", color: "#22c55e", bg: "rgba(34,197,94,0.18)" };
|
|
1210
|
-
return (_jsxs("button", { type: "button", onClick: () => onOpenComment(r.commentId), style: {
|
|
1211
|
-
display: "block",
|
|
1493
|
+
}, children: "\u00D7" })] }), _jsxs("div", { style: { padding: 12, borderBottom: "1px solid rgba(255,255,255,0.08)" }, children: [_jsx("button", { type: "button", onClick: onAddGeneral, style: {
|
|
1212
1494
|
width: "100%",
|
|
1213
|
-
|
|
1214
|
-
background: "rgba(255,
|
|
1215
|
-
|
|
1495
|
+
padding: "10px 12px",
|
|
1496
|
+
background: "rgba(255,107,107,0.12)",
|
|
1497
|
+
color: ACCENT,
|
|
1498
|
+
border: `1px dashed ${ACCENT}`,
|
|
1216
1499
|
borderRadius: 8,
|
|
1217
|
-
padding: 10,
|
|
1218
|
-
marginBottom: 6,
|
|
1219
1500
|
cursor: "pointer",
|
|
1220
|
-
color: "white",
|
|
1221
1501
|
font: "inherit",
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1502
|
+
fontWeight: 600,
|
|
1503
|
+
fontSize: 13,
|
|
1504
|
+
}, children: "+ Add note (about the page, not an element)" }), _jsx("button", { type: "button", onClick: isApproved ? undefined : onApprove, disabled: isApproved, title: isApproved ? "Mockup already approved" : "Approve the whole mockup (asks to confirm)", style: {
|
|
1505
|
+
width: "100%", marginTop: 8, padding: "10px 12px",
|
|
1506
|
+
background: isApproved ? "rgba(34,197,94,0.15)" : "rgba(34,197,94,0.10)",
|
|
1507
|
+
color: APPROVED_GREEN, border: `1px solid ${APPROVED_GREEN}`,
|
|
1508
|
+
borderRadius: 8, cursor: isApproved ? "default" : "pointer",
|
|
1509
|
+
font: "inherit", fontWeight: 700, fontSize: 13,
|
|
1510
|
+
}, children: isApproved ? "✓ Mockup approved" : "✅ Approve mockup" })] }), hiddenCount > 0 && (_jsx("button", { type: "button", onClick: onToggleApplied, style: { margin: "0 12px 8px", padding: "7px 10px", background: "transparent", color: "rgba(255,255,255,0.6)", border: "1px solid rgba(255,255,255,0.12)", borderRadius: 8, cursor: "pointer", font: "inherit", fontSize: 12 }, children: showApplied ? `Hide ${hiddenCount} already-applied` : `Show ${hiddenCount} already-applied` })), _jsx("div", { style: { overflow: "auto", flex: 1, padding: 8 }, children: visible.length === 0 ? (_jsx("div", { style: { padding: 16, opacity: 0.55, textAlign: "center", fontSize: 12 }, children: records.length === 0
|
|
1511
|
+
? "No comments yet. Toggle 💬 Comment + click an element to anchor a comment, or use the button above for a page-level note."
|
|
1512
|
+
: "Nothing needs attention — all comments are applied. Use the toggle above to see them." })) : ((() => {
|
|
1513
|
+
// #198-overlay — separate page-level notes from element-anchored
|
|
1514
|
+
// comments into labelled groups; element comments are the ones that
|
|
1515
|
+
// get sticky pins, page notes live only here.
|
|
1516
|
+
const rows = visible.slice().reverse();
|
|
1517
|
+
const groups = [
|
|
1518
|
+
{ label: "📍 On an element", items: rows.filter((r) => r.payload.element !== null) },
|
|
1519
|
+
{ label: "📄 On the page", items: rows.filter((r) => r.payload.element === null) },
|
|
1520
|
+
];
|
|
1521
|
+
const renderRow = (r) => {
|
|
1522
|
+
const status = r.plateReply?.status ?? null;
|
|
1523
|
+
const palette = pinPalette(status, false);
|
|
1524
|
+
const anchored = r.payload.element !== null;
|
|
1525
|
+
const live = anchored && typeof document !== "undefined"
|
|
1526
|
+
? resolveStoredSelector(document, r.payload.element.selector, r.payload.element.fallback_selector)
|
|
1527
|
+
: null;
|
|
1528
|
+
const hidden = anchored && live && (live.element.getBoundingClientRect().width === 0 ||
|
|
1529
|
+
live.element.offsetParent === null);
|
|
1530
|
+
const anchorLabel = !anchored
|
|
1531
|
+
? { text: "note", color: "#94a3b8", bg: "rgba(148,163,184,0.18)" }
|
|
1532
|
+
: !live
|
|
1533
|
+
? { text: "drifted", color: "#facc15", bg: "rgba(250,204,21,0.18)" }
|
|
1534
|
+
: hidden
|
|
1535
|
+
? { text: "hidden", color: "#94a3b8", bg: "rgba(148,163,184,0.18)" }
|
|
1536
|
+
: { text: "anchored", color: "#22c55e", bg: "rgba(34,197,94,0.18)" };
|
|
1537
|
+
const expanded = expandedId === r.commentId;
|
|
1538
|
+
const clamp = (lines) => expanded ? {} : { display: "-webkit-box", WebkitLineClamp: lines, WebkitBoxOrient: "vertical", overflow: "hidden" };
|
|
1539
|
+
return (_jsxs("div", { style: {
|
|
1540
|
+
background: "rgba(255,255,255,0.03)",
|
|
1541
|
+
border: `1px solid ${expanded ? "rgba(255,255,255,0.18)" : "rgba(255,255,255,0.08)"}`,
|
|
1542
|
+
borderRadius: 8, padding: 10, marginBottom: 6, color: "white",
|
|
1543
|
+
}, children: [_jsxs("button", { type: "button", onClick: () => setExpandedId(expanded ? null : r.commentId), style: { display: "flex", alignItems: "center", gap: 6, width: "100%", textAlign: "left", color: "white", font: "inherit", cursor: "pointer" }, children: [_jsxs("span", { style: { position: "relative", display: "inline-flex", flexShrink: 0 }, children: [_jsx("span", { style: { display: "inline-flex", alignItems: "center", justifyContent: "center", width: 18, height: 18, borderRadius: 999, background: authorColor(r.author || "unknown").bg, color: "#fff", fontSize: 9, fontWeight: 800 }, children: authorInitial(r.author || "unknown") }), status && (_jsx("span", { "aria-hidden": true, style: { position: "absolute", top: -4, right: -4, width: 10, height: 10, borderRadius: 999, background: palette.bg, color: palette.fg, border: "1.5px solid rgba(15,15,24,1)", fontSize: 7, fontWeight: 800, display: "flex", alignItems: "center", justifyContent: "center", lineHeight: 1 }, children: palette.glyph }))] }), _jsx("span", { style: { fontSize: 10, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.4, padding: "1px 6px", borderRadius: 999, background: anchorLabel.bg, color: anchorLabel.color }, children: anchorLabel.text }), _jsxs("span", { style: { fontSize: 10, opacity: 0.55, marginLeft: "auto" }, children: ["@", r.author, " \u00B7 ", formatTimeAgo(r.createdAt)] }), _jsx("span", { "aria-hidden": true, style: { fontSize: 10, opacity: 0.55, transform: expanded ? "rotate(90deg)" : "none", transition: "transform .15s" }, children: "\u25B6" })] }), _jsx("div", { style: { fontSize: 12, opacity: 0.85, lineHeight: 1.4, marginTop: 4, ...clamp(3) }, children: r.payload.prose }), r.plateReply?.summary && (_jsxs("div", { style: { marginTop: 6, paddingTop: 6, borderTop: "1px solid rgba(255,255,255,0.08)", fontSize: 11.5, lineHeight: 1.45, color: "rgba(255,255,255,0.82)", whiteSpace: "pre-wrap", ...clamp(4) }, children: [_jsx("span", { style: { color: palette.bg, fontWeight: 700 }, children: "\u21B3 reply: " }), r.plateReply.summary] })), _jsxs("div", { style: { display: "flex", gap: 12, marginTop: 8, fontSize: 11.5 }, children: [_jsx("a", { href: r.htmlUrl, target: "_blank", rel: "noreferrer", style: { color: "#7cc7ff", textDecoration: "none" }, children: "\u2197 Open issue on GitHub" }), anchored && (_jsxs("button", { type: "button", onClick: () => onOpenComment(r.commentId), style: { color: "#22c55e", font: "inherit", fontSize: 11.5, cursor: "pointer" }, children: ["\uD83D\uDCCD ", live ? "Locate on page" : "Anchor drifted"] })), !expanded && (r.payload.prose.length > 120 || (r.plateReply?.summary?.length ?? 0) > 180) && (_jsx("button", { type: "button", onClick: () => setExpandedId(r.commentId), style: { color: "rgba(255,255,255,0.6)", font: "inherit", fontSize: 11.5, cursor: "pointer", marginLeft: "auto" }, children: "Expand" }))] })] }, r.commentId));
|
|
1544
|
+
};
|
|
1545
|
+
return groups
|
|
1546
|
+
.filter((g) => g.items.length > 0)
|
|
1547
|
+
.map((g) => (_jsxs("div", { children: [_jsxs("div", { style: { fontSize: 10, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6, color: "rgba(255,255,255,0.42)", padding: "10px 6px 4px" }, children: [g.label, " \u00B7 ", g.items.length] }), g.items.map(renderRow)] }, g.label)));
|
|
1548
|
+
})()) })] }));
|
|
1244
1549
|
}
|
|
1245
1550
|
/**
|
|
1246
1551
|
* 0.5.0 — composer for general (no-anchor) comments. Same shape as the
|
|
@@ -1264,12 +1569,13 @@ function GeneralComposer(props) {
|
|
|
1264
1569
|
pointerEvents: "auto",
|
|
1265
1570
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
1266
1571
|
fontSize: 13,
|
|
1267
|
-
}, children: [_jsx("div", { style: { fontWeight: 600, marginBottom:
|
|
1572
|
+
}, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 6, fontSize: 14 }, children: "Add page note" }), _jsx(PageBadge, {}), _jsx("div", { style: { fontSize: 12, opacity: 0.65, marginBottom: 10 }, children: "Comment about overall behavior \u2014 not anchored to a specific element." }), _jsx("textarea", { "aria-label": "Note text", autoFocus: true, value: prose, onChange: (e) => setProse(e.target.value), placeholder: "e.g. 'Show an inline error when the user submits a duplicate name.'", rows: 5, style: {
|
|
1268
1573
|
width: "100%",
|
|
1269
1574
|
padding: 8,
|
|
1270
1575
|
border: "1px solid rgba(0,0,0,0.15)",
|
|
1271
1576
|
borderRadius: 6,
|
|
1272
1577
|
font: "inherit",
|
|
1578
|
+
fontSize: 16, // 0.6.12 — ≥16px stops iOS Safari auto-zooming on focus
|
|
1273
1579
|
resize: "vertical",
|
|
1274
1580
|
boxSizing: "border-box",
|
|
1275
1581
|
} }), _jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 12 }, children: [_jsx("button", { type: "button", onClick: props.onCancel, disabled: props.submitting, style: {
|
|
@@ -1292,4 +1598,121 @@ function GeneralComposer(props) {
|
|
|
1292
1598
|
fontWeight: 600,
|
|
1293
1599
|
}, children: props.submitting ? "Posting…" : "Post note" })] })] }));
|
|
1294
1600
|
}
|
|
1601
|
+
/**
|
|
1602
|
+
* 0.7.0 — Docs studio: the textual half of review. Reads the spine markdown docs
|
|
1603
|
+
* on the working branch, lets a reviewer edit them with an edit/preview toggle,
|
|
1604
|
+
* and (write-access only) commits the edit to the branch as a "scope change"
|
|
1605
|
+
* that `refine` reconciles. Non-write reviewers get read + preview.
|
|
1606
|
+
*/
|
|
1607
|
+
function DocsPanel(props) {
|
|
1608
|
+
const { repo, docPaths, branch, identity, getToken, onSignIn, apiBase, onClose, onFeedback } = props;
|
|
1609
|
+
const [path, setPath] = useState(docPaths[0] ?? "");
|
|
1610
|
+
const [file, setFile] = useState(null);
|
|
1611
|
+
const [draft, setDraft] = useState("");
|
|
1612
|
+
const [loading, setLoading] = useState(true);
|
|
1613
|
+
const [error, setError] = useState(null);
|
|
1614
|
+
const [view, setView] = useState("preview");
|
|
1615
|
+
const [submitting, setSubmitting] = useState(false);
|
|
1616
|
+
const canApply = identity?.canApply === true;
|
|
1617
|
+
const dirty = file != null && draft !== file.content;
|
|
1618
|
+
const ref = branch || "HEAD";
|
|
1619
|
+
const load = useCallback(async (p) => {
|
|
1620
|
+
setLoading(true);
|
|
1621
|
+
setError(null);
|
|
1622
|
+
setFile(null);
|
|
1623
|
+
const token = getToken();
|
|
1624
|
+
const res = await fetchDocFile({ owner: repo.owner, repo: repo.repo, path: p, ref, pat: token ?? undefined, apiBase });
|
|
1625
|
+
if (res.ok) {
|
|
1626
|
+
setFile(res.file);
|
|
1627
|
+
setDraft(res.file.content);
|
|
1628
|
+
setView("preview");
|
|
1629
|
+
}
|
|
1630
|
+
else if (!token && (res.status === 404 || res.status === 401)) {
|
|
1631
|
+
setError(`Sign in with GitHub to read ${p.split("/").pop()} — this repo is private.`);
|
|
1632
|
+
}
|
|
1633
|
+
else {
|
|
1634
|
+
setError(`Couldn't load ${p}: ${res.message} (${res.status})`);
|
|
1635
|
+
}
|
|
1636
|
+
setLoading(false);
|
|
1637
|
+
}, [repo.owner, repo.repo, ref, apiBase, getToken]);
|
|
1638
|
+
useEffect(() => { if (path)
|
|
1639
|
+
void load(path); }, [path, load]);
|
|
1640
|
+
async function submit() {
|
|
1641
|
+
if (!file || !dirty)
|
|
1642
|
+
return;
|
|
1643
|
+
const token = getToken();
|
|
1644
|
+
if (!canApply || !token)
|
|
1645
|
+
return;
|
|
1646
|
+
setSubmitting(true);
|
|
1647
|
+
const res = await commitDocFile({
|
|
1648
|
+
owner: repo.owner, repo: repo.repo, path, content: draft, sha: file.sha,
|
|
1649
|
+
branch: ref, message: `docs(scope): ${path} edited via review (PM scope change)`,
|
|
1650
|
+
pat: token, apiBase,
|
|
1651
|
+
});
|
|
1652
|
+
setSubmitting(false);
|
|
1653
|
+
if (res.ok) {
|
|
1654
|
+
onFeedback(`Scope change committed to ${ref} — refine will reconcile ${path.split("/").pop()}.`);
|
|
1655
|
+
await load(path); // refresh sha + content
|
|
1656
|
+
}
|
|
1657
|
+
else {
|
|
1658
|
+
setError(`Commit failed: ${res.message} (${res.status})`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const name = (p) => p.split("/").pop() ?? p;
|
|
1662
|
+
return (_jsxs("div", { "data-slowcook-overlay-ui": "1", role: "dialog", "aria-label": "Docs studio", onClick: (e) => e.stopPropagation(), style: {
|
|
1663
|
+
position: "fixed", inset: 0, zIndex: 2147483602,
|
|
1664
|
+
background: "rgba(15,15,24,0.97)", color: "white", pointerEvents: "auto",
|
|
1665
|
+
fontFamily: "system-ui, -apple-system, sans-serif", fontSize: 13,
|
|
1666
|
+
display: "flex", flexDirection: "column",
|
|
1667
|
+
}, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "12px 16px", borderBottom: "1px solid rgba(255,255,255,0.1)" }, children: [_jsx("span", { style: { fontWeight: 700, fontSize: 14 }, children: "\uD83D\uDCC4 Docs \u00B7 textual review" }), _jsxs("span", { style: { fontSize: 11, opacity: 0.6 }, children: ["branch ", _jsx("code", { style: { background: "rgba(255,255,255,0.1)", padding: "1px 6px", borderRadius: 5 }, children: ref })] }), _jsx("span", { style: { marginLeft: "auto" } }), _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close docs", style: { background: "transparent", border: "none", color: "rgba(255,255,255,0.6)", cursor: "pointer", fontSize: 22, lineHeight: 1 }, children: "\u00D7" })] }), _jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, padding: "10px 16px", borderBottom: "1px solid rgba(255,255,255,0.08)" }, children: docPaths.map((p) => (_jsxs("button", { type: "button", onClick: () => setPath(p), title: p, style: {
|
|
1668
|
+
fontSize: 12, fontWeight: p === path ? 800 : 600, padding: "5px 11px", borderRadius: 999,
|
|
1669
|
+
cursor: "pointer", border: "1px solid " + (p === path ? "rgba(255,107,107,0.7)" : "rgba(255,255,255,0.15)"),
|
|
1670
|
+
background: p === path ? "rgba(255,107,107,0.18)" : "transparent", color: p === path ? "#ffb4b4" : "rgba(255,255,255,0.75)",
|
|
1671
|
+
}, children: [name(p), dirty && p === path ? " •" : ""] }, p))) }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "8px 16px", borderBottom: "1px solid rgba(255,255,255,0.08)" }, children: [_jsx("div", { style: { display: "inline-flex", background: "rgba(255,255,255,0.06)", borderRadius: 999, padding: 2 }, children: ["preview", "edit"].map((v) => (_jsx("button", { type: "button", onClick: () => setView(v), style: { fontSize: 12, fontWeight: 700, padding: "4px 12px", borderRadius: 999, border: "none", cursor: "pointer",
|
|
1672
|
+
background: view === v ? "white" : "transparent", color: view === v ? "#111" : "rgba(255,255,255,0.7)" }, children: v === "preview" ? "Preview" : "Edit" }, v))) }), _jsx("span", { style: { marginLeft: "auto" } }), identity ? (canApply ? (_jsx("button", { type: "button", disabled: !dirty || submitting, onClick: () => void submit(), title: dirty ? "Commit this edit to the branch as a scope change" : "No changes yet", style: { fontSize: 12.5, fontWeight: 800, padding: "7px 14px", borderRadius: 8, border: "none",
|
|
1673
|
+
cursor: !dirty || submitting ? "not-allowed" : "pointer", opacity: !dirty || submitting ? 0.5 : 1,
|
|
1674
|
+
background: "#22c55e", color: "white" }, children: submitting ? "Submitting…" : "Submit scope change" })) : (_jsx("span", { style: { fontSize: 11.5, opacity: 0.7, maxWidth: 280 }, children: "Read-only \u2014 write access is required to submit a scope change; your notes can go via a review comment." }))) : (_jsx("button", { type: "button", onClick: onSignIn, style: { fontSize: 12.5, fontWeight: 700, padding: "7px 14px", borderRadius: 8, border: "none", cursor: "pointer", background: "white", color: "#111" }, children: "Sign in to edit" }))] }), _jsx("div", { style: { flex: 1, overflow: "auto", padding: 0 }, children: loading ? (_jsxs("div", { style: { padding: 24, opacity: 0.6 }, children: ["Loading ", name(path), "\u2026"] })) : error ? (_jsx("div", { style: { padding: 24, color: "#ffb4b4" }, children: error })) : view === "edit" ? (_jsx("textarea", { value: draft, onChange: (e) => setDraft(e.target.value), spellCheck: false, style: {
|
|
1675
|
+
width: "100%", height: "100%", minHeight: 400, boxSizing: "border-box",
|
|
1676
|
+
padding: "16px 20px", border: "none", outline: "none", resize: "none",
|
|
1677
|
+
background: "#11111b", color: "#e8e8f0", fontSize: 16, lineHeight: 1.55,
|
|
1678
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
1679
|
+
} })) : (_jsx("div", { className: "sc-doc-prose", style: { padding: "16px 24px", maxWidth: 860, margin: "0 auto", lineHeight: 1.6 }, dangerouslySetInnerHTML: { __html: renderMarkdown(draft) } })) }), _jsx("style", { dangerouslySetInnerHTML: { __html: `.sc-doc-prose h1,.sc-doc-prose h2,.sc-doc-prose h3{color:#fff;margin:1.1em 0 .4em;line-height:1.25}` +
|
|
1680
|
+
`.sc-doc-prose h1{font-size:24px}.sc-doc-prose h2{font-size:19px;border-bottom:1px solid rgba(255,255,255,0.12);padding-bottom:4px}.sc-doc-prose h3{font-size:16px}` +
|
|
1681
|
+
`.sc-doc-prose p,.sc-doc-prose li{color:rgba(255,255,255,0.85)}` +
|
|
1682
|
+
`.sc-doc-prose code{background:rgba(255,255,255,0.1);padding:1px 5px;border-radius:5px;font-size:0.9em}` +
|
|
1683
|
+
`.sc-doc-prose pre{background:#11111b;padding:12px 14px;border-radius:8px;overflow:auto}.sc-doc-prose pre code{background:none;padding:0}` +
|
|
1684
|
+
`.sc-doc-prose a{color:#7cc7ff}` +
|
|
1685
|
+
`.sc-doc-prose blockquote{border-left:3px solid rgba(255,107,107,0.6);margin:.6em 0;padding:.2em 0 .2em 14px;color:rgba(255,255,255,0.7)}` +
|
|
1686
|
+
`.sc-doc-prose hr{border:none;border-top:1px solid rgba(255,255,255,0.12);margin:1.2em 0}` +
|
|
1687
|
+
`.sc-doc-prose table{border-collapse:collapse;width:100%;margin:.8em 0;font-size:12.5px}` +
|
|
1688
|
+
`.sc-doc-prose th,.sc-doc-prose td{border:1px solid rgba(255,255,255,0.15);padding:6px 10px;text-align:left}` +
|
|
1689
|
+
`.sc-doc-prose th{background:rgba(255,255,255,0.06)}`
|
|
1690
|
+
} })] }));
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* 0.7.1 — "Viewing as" surface switcher, in the review pane. Lets a reviewer
|
|
1694
|
+
* jump between the mock's persona/section surfaces. This is review-only — a real
|
|
1695
|
+
* user is one role — so it must NOT live in the product mock. Tracks the current
|
|
1696
|
+
* route (longest matching surface home) and navigates via the parent's
|
|
1697
|
+
* router-agnostic handler.
|
|
1698
|
+
*/
|
|
1699
|
+
function SurfaceSwitcher(props) {
|
|
1700
|
+
const { surfaces, onNavigate, disabled } = props;
|
|
1701
|
+
const [path, setPath] = useState(typeof window !== "undefined" ? window.location.pathname : "/");
|
|
1702
|
+
useEffect(() => {
|
|
1703
|
+
if (typeof window === "undefined")
|
|
1704
|
+
return;
|
|
1705
|
+
const on = () => setPath(window.location.pathname);
|
|
1706
|
+
window.addEventListener("popstate", on);
|
|
1707
|
+
return () => window.removeEventListener("popstate", on);
|
|
1708
|
+
}, []);
|
|
1709
|
+
const current = surfaces
|
|
1710
|
+
.filter((s) => (s.home === "/" ? true : path === s.home || path.startsWith(s.home + "/") || path === s.home))
|
|
1711
|
+
.sort((a, b) => b.home.length - a.home.length)[0] ?? surfaces[0];
|
|
1712
|
+
return (_jsx("select", { className: "sc-ovl-pane-select", "aria-label": "Viewing as (review surface)", title: "Viewing as \u2014 jump between persona surfaces (review only; not a real control)", value: current?.home ?? "", disabled: disabled, onChange: (e) => onNavigate(e.target.value), style: {
|
|
1713
|
+
marginLeft: 2, maxWidth: 150,
|
|
1714
|
+
border: "1px solid rgba(0,0,0,0.15)", borderRadius: 999,
|
|
1715
|
+
cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? 0.6 : 1,
|
|
1716
|
+
}, children: surfaces.map((s) => (_jsx("option", { value: s.home, style: { color: "#111" }, children: (s.icon ? s.icon + " " : "") + "as " + s.label }, s.home))) }));
|
|
1717
|
+
}
|
|
1295
1718
|
//# sourceMappingURL=overlay.js.map
|