@gavin-hu/my-pi-agent 0.0.0-stage → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +160 -2
- package/assets/bytefly-chrome.png +0 -0
- package/assets/bytefly.schema.json +769 -0
- package/assets/file-browse.png +0 -0
- package/assets/plan-review.png +0 -0
- package/extensions/bytefly/README.md +820 -0
- package/extensions/bytefly/doctor/commands.ts +68 -0
- package/extensions/bytefly/doctor/config.ts +47 -0
- package/extensions/bytefly/doctor/format.ts +188 -0
- package/extensions/bytefly/doctor/register.ts +54 -0
- package/extensions/bytefly/doctor/runtime.ts +87 -0
- package/extensions/bytefly/doctor/schema.ts +51 -0
- package/extensions/bytefly/doctor/tools.ts +113 -0
- package/extensions/bytefly/doctor/tui.ts +140 -0
- package/extensions/bytefly/footer/collect/entries.ts +137 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +64 -0
- package/extensions/bytefly/footer/config.ts +35 -0
- package/extensions/bytefly/footer/price/config.ts +152 -0
- package/extensions/bytefly/footer/price/price.ts +59 -0
- package/extensions/bytefly/footer/price/rates.ts +54 -0
- package/extensions/bytefly/footer/price/schedule.ts +112 -0
- package/extensions/bytefly/footer/price/types.ts +53 -0
- package/extensions/bytefly/footer/register.ts +116 -0
- package/extensions/bytefly/footer/render/footer.ts +51 -0
- package/extensions/bytefly/footer/render/format.ts +106 -0
- package/extensions/bytefly/footer/render/layout.ts +139 -0
- package/extensions/bytefly/footer/render/lines.ts +50 -0
- package/extensions/bytefly/footer/render/segments.ts +281 -0
- package/extensions/bytefly/footer/render/status.ts +42 -0
- package/extensions/bytefly/footer/types.ts +104 -0
- package/extensions/bytefly/header/component.ts +112 -0
- package/extensions/bytefly/header/config.ts +77 -0
- package/extensions/bytefly/header/register.ts +53 -0
- package/extensions/bytefly/index.ts +48 -0
- package/extensions/bytefly/notify/config.ts +60 -0
- package/extensions/bytefly/notify/dispatch.ts +138 -0
- package/extensions/bytefly/notify/format.ts +39 -0
- package/extensions/bytefly/notify/register.ts +85 -0
- package/extensions/bytefly/notify/types.ts +28 -0
- package/extensions/bytefly/picker/commands.ts +73 -0
- package/extensions/bytefly/picker/discovery.ts +128 -0
- package/extensions/bytefly/picker/resources.ts +226 -0
- package/extensions/bytefly/picker/runtime.ts +108 -0
- package/extensions/bytefly/picker/toggle.ts +145 -0
- package/extensions/bytefly/picker/tui.ts +260 -0
- package/extensions/bytefly/picker/types.ts +55 -0
- package/extensions/bytefly/separator/component.ts +26 -0
- package/extensions/bytefly/separator/config.ts +24 -0
- package/extensions/bytefly/separator/format.ts +50 -0
- package/extensions/bytefly/separator/register.ts +42 -0
- package/extensions/bytefly/separator/types.ts +22 -0
- package/extensions/document/README.md +281 -0
- package/extensions/document/config.ts +58 -0
- package/extensions/document/extract/doc.ts +19 -0
- package/extensions/document/extract/docx.ts +18 -0
- package/extensions/document/extract/file.ts +45 -0
- package/extensions/document/extract/odt.ts +101 -0
- package/extensions/document/extract/pdf.ts +25 -0
- package/extensions/document/extract/rtf.ts +45 -0
- package/extensions/document/extract/vendor.d.ts +39 -0
- package/extensions/document/extract/xlsx.ts +88 -0
- package/extensions/document/formats.ts +59 -0
- package/extensions/document/index.ts +17 -0
- package/extensions/document/paging.ts +110 -0
- package/extensions/document/render.ts +105 -0
- package/extensions/document/schema.ts +50 -0
- package/extensions/document/tool.ts +152 -0
- package/extensions/file-browse/README.md +269 -0
- package/extensions/file-browse/client.ts +550 -0
- package/extensions/file-browse/config.ts +82 -0
- package/extensions/file-browse/files.ts +433 -0
- package/extensions/file-browse/fuzzy.ts +75 -0
- package/extensions/file-browse/git.ts +74 -0
- package/extensions/file-browse/html.ts +346 -0
- package/extensions/file-browse/icons.ts +119 -0
- package/extensions/file-browse/index.ts +149 -0
- package/extensions/file-browse/markdown.ts +592 -0
- package/extensions/file-browse/paths.ts +101 -0
- package/extensions/file-browse/router.ts +289 -0
- package/extensions/file-browse/server.ts +104 -0
- package/extensions/file-open/README.md +135 -0
- package/extensions/file-open/index.ts +93 -0
- package/extensions/file-open/open.ts +102 -0
- package/extensions/file-open/schema.ts +31 -0
- package/extensions/goal/README.md +241 -0
- package/extensions/goal/clarify-parse.ts +203 -0
- package/extensions/goal/clarify-prompt.ts +80 -0
- package/extensions/goal/clarify.ts +234 -0
- package/extensions/goal/commands.ts +220 -0
- package/extensions/goal/config.ts +35 -0
- package/extensions/goal/format.ts +111 -0
- package/extensions/goal/index.ts +77 -0
- package/extensions/goal/runtime.ts +90 -0
- package/extensions/goal/schema.ts +105 -0
- package/extensions/goal/state.ts +69 -0
- package/extensions/goal/tools.ts +136 -0
- package/extensions/goal/tui.ts +97 -0
- package/extensions/goal/types.ts +42 -0
- package/extensions/job/README.md +309 -0
- package/extensions/job/commands.ts +74 -0
- package/extensions/job/config.ts +68 -0
- package/extensions/job/format.ts +256 -0
- package/extensions/job/index.ts +140 -0
- package/extensions/job/logs.ts +56 -0
- package/extensions/job/paths.ts +31 -0
- package/extensions/job/process.ts +106 -0
- package/extensions/job/registry.ts +265 -0
- package/extensions/job/reservations.ts +82 -0
- package/extensions/job/runtime.ts +846 -0
- package/extensions/job/schema.ts +148 -0
- package/extensions/job/session.ts +116 -0
- package/extensions/job/status.ts +69 -0
- package/extensions/job/store.ts +103 -0
- package/extensions/job/tools.ts +295 -0
- package/extensions/job/tui.ts +486 -0
- package/extensions/job/types.ts +92 -0
- package/extensions/job/ui.ts +99 -0
- package/extensions/job/waiters.ts +46 -0
- package/extensions/memory/README.md +221 -0
- package/extensions/memory/commands.ts +190 -0
- package/extensions/memory/config.ts +35 -0
- package/extensions/memory/errors.ts +9 -0
- package/extensions/memory/format.ts +141 -0
- package/extensions/memory/index.ts +59 -0
- package/extensions/memory/runtime.ts +199 -0
- package/extensions/memory/schema.ts +86 -0
- package/extensions/memory/store.ts +231 -0
- package/extensions/memory/tools.ts +165 -0
- package/extensions/memory/tui.ts +430 -0
- package/extensions/memory/types.ts +38 -0
- package/extensions/plan/README.md +417 -0
- package/extensions/plan/blocks.ts +197 -0
- package/extensions/plan/commands.ts +134 -0
- package/extensions/plan/feedback.ts +157 -0
- package/extensions/plan/git-bash.ts +320 -0
- package/extensions/plan/index.ts +179 -0
- package/extensions/plan/path-guard.ts +101 -0
- package/extensions/plan/plans.ts +267 -0
- package/extensions/plan/policy.ts +73 -0
- package/extensions/plan/runtime.ts +195 -0
- package/extensions/plan/steps.ts +228 -0
- package/extensions/plan/tools.ts +605 -0
- package/extensions/plan/tui.ts +391 -0
- package/extensions/plan/types.ts +67 -0
- package/extensions/plan/web-client.ts +433 -0
- package/extensions/plan/web-page.ts +169 -0
- package/extensions/plan/web-router.ts +155 -0
- package/extensions/plan/web.ts +267 -0
- package/extensions/question/README.md +189 -0
- package/extensions/question/answers.ts +119 -0
- package/extensions/question/dialogs.ts +109 -0
- package/extensions/question/index.ts +28 -0
- package/extensions/question/schema.ts +119 -0
- package/extensions/question/tools.ts +122 -0
- package/extensions/question/tui-state.ts +240 -0
- package/extensions/question/tui.ts +324 -0
- package/extensions/question/types.ts +61 -0
- package/extensions/rewind/README.md +202 -0
- package/extensions/rewind/commands.ts +74 -0
- package/extensions/rewind/config.ts +71 -0
- package/extensions/rewind/format.ts +110 -0
- package/extensions/rewind/git.ts +177 -0
- package/extensions/rewind/index.ts +60 -0
- package/extensions/rewind/policy.ts +42 -0
- package/extensions/rewind/queue.ts +27 -0
- package/extensions/rewind/restore.ts +96 -0
- package/extensions/rewind/rewind-tui.ts +135 -0
- package/extensions/rewind/rewind.ts +146 -0
- package/extensions/rewind/runtime.ts +174 -0
- package/extensions/rewind/snapshot-run.ts +103 -0
- package/extensions/rewind/snapshot.ts +80 -0
- package/extensions/rewind/status.ts +67 -0
- package/extensions/rewind/store.ts +142 -0
- package/extensions/rewind/temp-index.ts +87 -0
- package/extensions/rewind/timeline.ts +119 -0
- package/extensions/rewind/types.ts +58 -0
- package/extensions/subagent/README.md +310 -0
- package/extensions/subagent/agents.ts +454 -0
- package/extensions/subagent/format.ts +126 -0
- package/extensions/subagent/index.ts +181 -0
- package/extensions/subagent/invocation.ts +57 -0
- package/extensions/subagent/orchestrate.ts +182 -0
- package/extensions/subagent/render.ts +491 -0
- package/extensions/subagent/run.ts +212 -0
- package/extensions/subagent/schema.ts +102 -0
- package/extensions/subagent/stream.ts +144 -0
- package/extensions/subagent/types.ts +79 -0
- package/extensions/todo/README.md +230 -0
- package/extensions/todo/commands.ts +41 -0
- package/extensions/todo/config.ts +29 -0
- package/extensions/todo/format.ts +142 -0
- package/extensions/todo/index.ts +100 -0
- package/extensions/todo/nudge.ts +64 -0
- package/extensions/todo/runtime.ts +149 -0
- package/extensions/todo/schema.ts +125 -0
- package/extensions/todo/state.ts +65 -0
- package/extensions/todo/tools.ts +136 -0
- package/extensions/todo/tui.ts +170 -0
- package/extensions/todo/types.ts +33 -0
- package/extensions/watch/README.md +214 -0
- package/extensions/watch/commands.ts +144 -0
- package/extensions/watch/config.ts +59 -0
- package/extensions/watch/format.ts +212 -0
- package/extensions/watch/index.ts +129 -0
- package/extensions/watch/process.ts +63 -0
- package/extensions/watch/runtime.ts +525 -0
- package/extensions/watch/schema.ts +177 -0
- package/extensions/watch/tools.ts +278 -0
- package/extensions/watch/tui.ts +427 -0
- package/extensions/watch/types.ts +83 -0
- package/extensions/watch/ui.ts +97 -0
- package/extensions/web-access/README.md +347 -0
- package/extensions/web-access/config.ts +36 -0
- package/extensions/web-access/fetch/cache.ts +78 -0
- package/extensions/web-access/fetch/config.ts +81 -0
- package/extensions/web-access/fetch/extract.ts +124 -0
- package/extensions/web-access/fetch/find.ts +158 -0
- package/extensions/web-access/fetch/format.ts +117 -0
- package/extensions/web-access/fetch/page.ts +169 -0
- package/extensions/web-access/fetch/pdf.ts +45 -0
- package/extensions/web-access/fetch/schema.ts +192 -0
- package/extensions/web-access/fetch/ssrf.ts +191 -0
- package/extensions/web-access/fetch/tool.ts +238 -0
- package/extensions/web-access/fetch/types.ts +26 -0
- package/extensions/web-access/html.ts +41 -0
- package/extensions/web-access/http.ts +133 -0
- package/extensions/web-access/index.ts +28 -0
- package/extensions/web-access/search/config.ts +115 -0
- package/extensions/web-access/search/format.ts +65 -0
- package/extensions/web-access/search/json.ts +75 -0
- package/extensions/web-access/search/provider.ts +38 -0
- package/extensions/web-access/search/providers/brave.ts +76 -0
- package/extensions/web-access/search/providers/duckduckgo.ts +109 -0
- package/extensions/web-access/search/providers/searxng.ts +98 -0
- package/extensions/web-access/search/registry.ts +49 -0
- package/extensions/web-access/search/schema.ts +65 -0
- package/extensions/web-access/search/search.ts +129 -0
- package/extensions/web-access/search/tool.ts +112 -0
- package/extensions/web-access/search/types.ts +32 -0
- package/extensions/web-access/text.ts +26 -0
- package/extensions/web-access/transcript.ts +20 -0
- package/extensions/wechat/README.md +273 -0
- package/extensions/wechat/bridge.ts +577 -0
- package/extensions/wechat/client.ts +411 -0
- package/extensions/wechat/commands.ts +220 -0
- package/extensions/wechat/config.ts +88 -0
- package/extensions/wechat/credentials.ts +89 -0
- package/extensions/wechat/format.ts +76 -0
- package/extensions/wechat/index.ts +175 -0
- package/extensions/wechat/interaction.ts +130 -0
- package/extensions/wechat/lock.ts +86 -0
- package/extensions/wechat/login-screen.ts +91 -0
- package/extensions/wechat/login.ts +130 -0
- package/extensions/wechat/media.ts +103 -0
- package/extensions/wechat/outbound.ts +102 -0
- package/extensions/wechat/prompt.ts +104 -0
- package/extensions/wechat/qrcode.ts +46 -0
- package/extensions/wechat/remote-ui.ts +97 -0
- package/extensions/wechat/slash.ts +114 -0
- package/extensions/wechat/state.ts +71 -0
- package/extensions/wechat/status-screen.ts +176 -0
- package/extensions/wechat/tools.ts +109 -0
- package/extensions/wechat/types.ts +170 -0
- package/extensions/worktree/README.md +278 -0
- package/extensions/worktree/commands.ts +110 -0
- package/extensions/worktree/config.ts +107 -0
- package/extensions/worktree/enter.ts +273 -0
- package/extensions/worktree/errors.ts +18 -0
- package/extensions/worktree/exit.ts +122 -0
- package/extensions/worktree/git.ts +429 -0
- package/extensions/worktree/guard.ts +566 -0
- package/extensions/worktree/include.ts +147 -0
- package/extensions/worktree/index.ts +221 -0
- package/extensions/worktree/lifecycle.ts +14 -0
- package/extensions/worktree/prune.ts +99 -0
- package/extensions/worktree/registry.ts +221 -0
- package/extensions/worktree/root-tools.ts +162 -0
- package/extensions/worktree/runtime.ts +135 -0
- package/extensions/worktree/state.ts +65 -0
- package/extensions/worktree/status.ts +115 -0
- package/extensions/worktree/tools.ts +147 -0
- package/lib/README.md +49 -0
- package/lib/browser.ts +40 -0
- package/lib/config.ts +186 -0
- package/lib/env.ts +41 -0
- package/lib/escape.ts +27 -0
- package/lib/events.ts +40 -0
- package/lib/format.ts +76 -0
- package/lib/git/index.ts +7 -0
- package/lib/git/read.ts +76 -0
- package/lib/git/runner.ts +80 -0
- package/lib/highlight.ts +1000 -0
- package/lib/interaction.ts +132 -0
- package/lib/list-cursor.ts +222 -0
- package/lib/path.ts +49 -0
- package/lib/pi-invocation.ts +71 -0
- package/lib/policy.ts +67 -0
- package/lib/rails.ts +91 -0
- package/lib/requirements.ts +431 -0
- package/lib/shell.ts +33 -0
- package/lib/tool-names.ts +32 -0
- package/lib/tui.ts +184 -0
- package/lib/ui.ts +105 -0
- package/lib/web/base.ts +18 -0
- package/lib/web/components.ts +122 -0
- package/lib/web/index.ts +12 -0
- package/lib/web/layout.ts +30 -0
- package/lib/web/markup.ts +99 -0
- package/lib/web/runtime.ts +68 -0
- package/lib/web/tokens.ts +128 -0
- package/package.json +134 -4
- package/themes/bytefly-dark.json +91 -0
- package/themes/bytefly-light.json +91 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser review client, served as `/client.js`.
|
|
3
|
+
*
|
|
4
|
+
* Kept as one self-contained script (like `file-browse/client.ts`) with no
|
|
5
|
+
* build step. It renders the plan from the JSON data island using DOM text
|
|
6
|
+
* nodes only — plan and comment text never becomes markup — renders each block
|
|
7
|
+
* with its comment threads indented directly beneath it, highlights code by
|
|
8
|
+
* copying only `tok-*` spans through `DOMParser`, builds the section outline,
|
|
9
|
+
* and saves the annotation list back to the server. An `EventSource` keeps a
|
|
10
|
+
* connection open so the server can tell when the tab is gone; the explicit
|
|
11
|
+
* buttons are the primary save/cancel path.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { THEME_TOGGLE_JS } from "../../lib/web/index.ts";
|
|
15
|
+
|
|
16
|
+
export const REVIEW_CLIENT_JS = String.raw`(function () {
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
var dataNode = document.getElementById("review-data");
|
|
20
|
+
var fallback = { blocks: [], annotations: [], outline: [], code: {}, token: "" };
|
|
21
|
+
var data = fallback;
|
|
22
|
+
if (dataNode && dataNode.textContent) {
|
|
23
|
+
try { data = JSON.parse(dataNode.textContent); } catch (error) { data = fallback; }
|
|
24
|
+
}
|
|
25
|
+
var token = typeof data.token === "string" ? data.token : "";
|
|
26
|
+
var annotations = Array.isArray(data.annotations) ? data.annotations.slice() : [];
|
|
27
|
+
var saved = false;
|
|
28
|
+
var closed = false;
|
|
29
|
+
var sectionObserver = null;
|
|
30
|
+
var pendingQuote = null;
|
|
31
|
+
|
|
32
|
+
var planEl = document.getElementById("plan");
|
|
33
|
+
var statusEl = document.getElementById("status");
|
|
34
|
+
var saveButton = document.getElementById("save");
|
|
35
|
+
var cancelButton = document.getElementById("cancel");
|
|
36
|
+
var outlineList = document.getElementById("outline-list");
|
|
37
|
+
|
|
38
|
+
function el(tag, className) {
|
|
39
|
+
var node = document.createElement(tag);
|
|
40
|
+
if (className) node.className = className;
|
|
41
|
+
return node;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function setStatus(text, kind) {
|
|
45
|
+
if (!statusEl) return;
|
|
46
|
+
statusEl.textContent = text || "";
|
|
47
|
+
statusEl.title = text || "";
|
|
48
|
+
statusEl.className = kind ? "bf-status bf-status--" + kind : "bf-status";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function safeHref(url) {
|
|
52
|
+
var trimmed = String(url).trim();
|
|
53
|
+
if (/^(https?:|mailto:)/i.test(trimmed)) return trimmed;
|
|
54
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return null;
|
|
55
|
+
return trimmed;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var INLINE = /(\x60[^\x60]+\x60|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\))/g;
|
|
59
|
+
|
|
60
|
+
function renderInline(container, text) {
|
|
61
|
+
String(text).split(INLINE).forEach(function (part) {
|
|
62
|
+
if (!part) return;
|
|
63
|
+
var match;
|
|
64
|
+
if ((match = /^\x60([^\x60]+)\x60$/.exec(part))) {
|
|
65
|
+
var code = el("code"); code.textContent = match[1]; container.appendChild(code); return;
|
|
66
|
+
}
|
|
67
|
+
if ((match = /^\*\*([^*]+)\*\*$/.exec(part))) {
|
|
68
|
+
var strong = el("strong"); strong.textContent = match[1]; container.appendChild(strong); return;
|
|
69
|
+
}
|
|
70
|
+
if ((match = /^\*([^*]+)\*$/.exec(part))) {
|
|
71
|
+
var em = el("em"); em.textContent = match[1]; container.appendChild(em); return;
|
|
72
|
+
}
|
|
73
|
+
if ((match = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(part))) {
|
|
74
|
+
var href = safeHref(match[2]);
|
|
75
|
+
if (!href) { container.appendChild(document.createTextNode(part)); return; }
|
|
76
|
+
var link = el("a"); link.textContent = match[1]; link.href = href; link.target = "_blank"; link.rel = "noreferrer noopener";
|
|
77
|
+
container.appendChild(link); return;
|
|
78
|
+
}
|
|
79
|
+
container.appendChild(document.createTextNode(part));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Copy only text and tok-* spans out of server-highlighted HTML, so a code
|
|
84
|
+
// line can never introduce markup even though it arrives as a string.
|
|
85
|
+
function copyTokens(from, to) {
|
|
86
|
+
Array.prototype.forEach.call(from.childNodes, function (node) {
|
|
87
|
+
if (node.nodeType === 3) { to.appendChild(document.createTextNode(node.nodeValue)); return; }
|
|
88
|
+
if (node.nodeType === 1 && node.tagName === "SPAN" && /^tok-/.test(node.className || "")) {
|
|
89
|
+
var span = el("span", node.className);
|
|
90
|
+
copyTokens(node, span);
|
|
91
|
+
to.appendChild(span);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
to.appendChild(document.createTextNode(node.textContent || ""));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function codeBody(lines) {
|
|
99
|
+
var body = Array.isArray(lines) ? lines.slice() : [];
|
|
100
|
+
if (body.length && /^ {0,3}(\x60{3,}|~{3,})/.test(body[0])) body.shift();
|
|
101
|
+
if (body.length && /^ {0,3}(\x60{3,}|~{3,})/.test(body[body.length - 1])) body.pop();
|
|
102
|
+
return body;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function renderCode(wrap, block) {
|
|
106
|
+
var body = codeBody(block.lines);
|
|
107
|
+
var highlighted = data.code && data.code[String(block.start)];
|
|
108
|
+
var pre = el("pre", "bf-code");
|
|
109
|
+
var code = el("code");
|
|
110
|
+
if (Array.isArray(highlighted)) {
|
|
111
|
+
body.forEach(function (line, index) {
|
|
112
|
+
var span = el("span", "bf-code__line");
|
|
113
|
+
var html = highlighted[index];
|
|
114
|
+
if (html) {
|
|
115
|
+
try { copyTokens(new DOMParser().parseFromString(html, "text/html").body, span); }
|
|
116
|
+
catch (error) { span.textContent = line; }
|
|
117
|
+
} else {
|
|
118
|
+
span.textContent = line;
|
|
119
|
+
}
|
|
120
|
+
code.appendChild(span);
|
|
121
|
+
if (index < body.length - 1) code.appendChild(document.createTextNode("\n"));
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
code.textContent = body.join("\n");
|
|
125
|
+
}
|
|
126
|
+
pre.appendChild(code);
|
|
127
|
+
wrap.appendChild(pre);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderBlockContent(wrap, block) {
|
|
131
|
+
var lines = Array.isArray(block.lines) ? block.lines : [];
|
|
132
|
+
if (block.kind === "heading") {
|
|
133
|
+
var text = lines.join(" ").replace(/^\s*#{1,6}\s*/, "");
|
|
134
|
+
var level = Math.min(6, Math.max(1, (lines[0].match(/^ {0,3}(#{1,6})/) || ["", "#"])[1].length));
|
|
135
|
+
var heading = el("h" + level);
|
|
136
|
+
renderInline(heading, text);
|
|
137
|
+
wrap.appendChild(heading);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (block.kind === "code") { renderCode(wrap, block); return; }
|
|
141
|
+
if (block.kind === "quote") {
|
|
142
|
+
var quote = el("blockquote");
|
|
143
|
+
renderInline(quote, lines.map(function (line) { return line.replace(/^\s*>\s?/, ""); }).join("\n"));
|
|
144
|
+
wrap.appendChild(quote);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (block.kind === "list") {
|
|
148
|
+
lines.forEach(function (line) {
|
|
149
|
+
var match = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(line);
|
|
150
|
+
if (!match) {
|
|
151
|
+
var plain = el("p"); renderInline(plain, line); wrap.appendChild(plain); return;
|
|
152
|
+
}
|
|
153
|
+
var row = el("div", "bf-block__li");
|
|
154
|
+
row.style.paddingLeft = String(Math.floor(match[1].replace(/\t/g, " ").length / 2) * 16) + "px";
|
|
155
|
+
var markerText = match[2];
|
|
156
|
+
var rest = match[3];
|
|
157
|
+
var box = /^\[([ xX])\]\s*(.*)$/.exec(rest);
|
|
158
|
+
var marker = el("span", "marker");
|
|
159
|
+
if (box) {
|
|
160
|
+
var done = box[1].toLowerCase() === "x";
|
|
161
|
+
markerText = done ? "\u2713" : "\u2610";
|
|
162
|
+
marker.className = done ? "bf-block__marker bf-block__marker--done" : "bf-block__marker";
|
|
163
|
+
rest = box[2];
|
|
164
|
+
}
|
|
165
|
+
marker.textContent = markerText;
|
|
166
|
+
var body = el("span"); renderInline(body, rest);
|
|
167
|
+
row.appendChild(marker); row.appendChild(body); wrap.appendChild(row);
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
var paragraph = el("p");
|
|
172
|
+
renderInline(paragraph, lines.join(" "));
|
|
173
|
+
wrap.appendChild(paragraph);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function closeComposer() {
|
|
177
|
+
var open = planEl ? planEl.querySelector(".bf-composer") : null;
|
|
178
|
+
if (open && open.parentNode) open.parentNode.removeChild(open);
|
|
179
|
+
Array.prototype.forEach.call(planEl ? planEl.querySelectorAll(".bf-block--composing") : [], function (block) {
|
|
180
|
+
block.classList.remove("bf-block--composing");
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function selectionIn(wrap) {
|
|
185
|
+
var selection = window.getSelection && window.getSelection();
|
|
186
|
+
if (!selection || selection.isCollapsed) return "";
|
|
187
|
+
var range = selection.rangeCount ? selection.getRangeAt(0) : null;
|
|
188
|
+
if (!range || !wrap.contains(range.commonAncestorContainer)) return "";
|
|
189
|
+
return selection.toString().replace(/\s+/g, " ").trim().slice(0, 500);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function commentsBoxAfter(wrap) {
|
|
193
|
+
var next = wrap.nextElementSibling;
|
|
194
|
+
if (next && next.classList && next.classList.contains("bf-comments")) return next;
|
|
195
|
+
var box = el("div", "bf-comments");
|
|
196
|
+
wrap.insertAdjacentElement("afterend", box);
|
|
197
|
+
return box;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function openComposer(wrap, block, quote) {
|
|
201
|
+
closeComposer();
|
|
202
|
+
wrap.classList.add("bf-block--composing");
|
|
203
|
+
var composer = el("div", "bf-composer");
|
|
204
|
+
if (quote) {
|
|
205
|
+
var chip = el("div", "bf-quote-chip");
|
|
206
|
+
chip.textContent = "\u201c" + quote + "\u201d";
|
|
207
|
+
composer.appendChild(chip);
|
|
208
|
+
}
|
|
209
|
+
var area = el("textarea", "bf-textarea");
|
|
210
|
+
area.placeholder = "Leave feedback on this part of the plan";
|
|
211
|
+
area.setAttribute("aria-label", "Comment");
|
|
212
|
+
composer.appendChild(area);
|
|
213
|
+
var row = el("div", "bf-composer__row");
|
|
214
|
+
var cancel = el("button"); cancel.type = "button"; cancel.className = "bf-btn"; cancel.textContent = "Cancel";
|
|
215
|
+
cancel.addEventListener("click", closeComposer);
|
|
216
|
+
var add = el("button"); add.type = "button"; add.className = "bf-btn bf-btn--primary"; add.textContent = "Add comment";
|
|
217
|
+
add.addEventListener("click", function () {
|
|
218
|
+
var comment = area.value.replace(/\s+/g, " ").trim();
|
|
219
|
+
if (!comment) { area.focus(); return; }
|
|
220
|
+
annotations.push(quote ? { anchor: block.start, quote: quote, comment: comment } : { anchor: block.start, comment: comment });
|
|
221
|
+
renderPlan();
|
|
222
|
+
});
|
|
223
|
+
row.appendChild(cancel); row.appendChild(add); composer.appendChild(row);
|
|
224
|
+
commentsBoxAfter(wrap).appendChild(composer);
|
|
225
|
+
area.focus();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function startEdit(card, index) {
|
|
229
|
+
var annotation = annotations[index];
|
|
230
|
+
card.textContent = "";
|
|
231
|
+
var area = el("textarea", "bf-textarea");
|
|
232
|
+
area.value = annotation.comment;
|
|
233
|
+
area.setAttribute("aria-label", "Edit comment");
|
|
234
|
+
card.appendChild(area);
|
|
235
|
+
var row = el("div", "bf-comment__row");
|
|
236
|
+
var cancel = el("button"); cancel.type = "button"; cancel.className = "bf-btn"; cancel.textContent = "Cancel";
|
|
237
|
+
cancel.addEventListener("click", renderPlan);
|
|
238
|
+
var apply = el("button"); apply.type = "button"; apply.className = "bf-btn bf-btn--primary"; apply.textContent = "Save";
|
|
239
|
+
apply.addEventListener("click", function () {
|
|
240
|
+
var comment = area.value.replace(/\s+/g, " ").trim();
|
|
241
|
+
if (!comment) { area.focus(); return; }
|
|
242
|
+
annotation.comment = comment;
|
|
243
|
+
renderPlan();
|
|
244
|
+
});
|
|
245
|
+
row.appendChild(cancel); row.appendChild(apply); card.appendChild(row);
|
|
246
|
+
area.focus();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function renderCommentCards(box, list) {
|
|
250
|
+
list.forEach(function (annotation) {
|
|
251
|
+
var index = annotations.indexOf(annotation);
|
|
252
|
+
var card = el("div", "bf-comment");
|
|
253
|
+
if (annotation.quote) {
|
|
254
|
+
var quote = el("div", "bf-comment__quote");
|
|
255
|
+
quote.textContent = "\u201c" + annotation.quote + "\u201d";
|
|
256
|
+
card.appendChild(quote);
|
|
257
|
+
}
|
|
258
|
+
var body = el("div", "bf-comment__body");
|
|
259
|
+
body.textContent = annotation.comment;
|
|
260
|
+
card.appendChild(body);
|
|
261
|
+
var row = el("div", "bf-comment__row");
|
|
262
|
+
var edit = el("button"); edit.type = "button"; edit.className = "bf-btn"; edit.textContent = "Edit";
|
|
263
|
+
edit.addEventListener("click", function () { startEdit(card, index); });
|
|
264
|
+
var remove = el("button"); remove.type = "button"; remove.className = "bf-btn"; remove.textContent = "Delete";
|
|
265
|
+
remove.addEventListener("click", function () { annotations.splice(index, 1); renderPlan(); });
|
|
266
|
+
row.appendChild(edit); row.appendChild(remove); card.appendChild(row);
|
|
267
|
+
box.appendChild(card);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function renderComments(container, anchor) {
|
|
272
|
+
var mine = annotations.filter(function (annotation) { return annotation.anchor === anchor; });
|
|
273
|
+
if (mine.length === 0) return;
|
|
274
|
+
var box = el("div", "bf-comments");
|
|
275
|
+
renderCommentCards(box, mine);
|
|
276
|
+
container.appendChild(box);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function observeSections() {
|
|
280
|
+
if (sectionObserver && sectionObserver.disconnect) sectionObserver.disconnect();
|
|
281
|
+
sectionObserver = null;
|
|
282
|
+
if (!outlineList || !window.IntersectionObserver) return;
|
|
283
|
+
var links = {};
|
|
284
|
+
Array.prototype.forEach.call(outlineList.querySelectorAll("a"), function (link) {
|
|
285
|
+
links[link.getAttribute("data-id")] = link;
|
|
286
|
+
});
|
|
287
|
+
var headings = Array.prototype.slice.call(planEl.querySelectorAll(".bf-block[id]"));
|
|
288
|
+
if (headings.length === 0) return;
|
|
289
|
+
sectionObserver = new IntersectionObserver(function (entries) {
|
|
290
|
+
entries.forEach(function (entry) {
|
|
291
|
+
if (!entry.isIntersecting) return;
|
|
292
|
+
Array.prototype.forEach.call(outlineList.querySelectorAll("a"), function (link) {
|
|
293
|
+
link.removeAttribute("aria-current");
|
|
294
|
+
});
|
|
295
|
+
var link = links[entry.target.id];
|
|
296
|
+
if (link) link.setAttribute("aria-current", "true");
|
|
297
|
+
});
|
|
298
|
+
}, { rootMargin: "-76px 0px -68% 0px", threshold: 0 });
|
|
299
|
+
headings.forEach(function (heading) { sectionObserver.observe(heading); });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function renderOutline() {
|
|
303
|
+
if (!outlineList) return;
|
|
304
|
+
outlineList.replaceChildren();
|
|
305
|
+
var items = Array.isArray(data.outline) ? data.outline : [];
|
|
306
|
+
items.forEach(function (item) {
|
|
307
|
+
var li = el("li", "lvl-" + item.level);
|
|
308
|
+
var link = el("a");
|
|
309
|
+
link.href = "#" + item.id;
|
|
310
|
+
link.setAttribute("data-id", item.id);
|
|
311
|
+
link.textContent = item.text;
|
|
312
|
+
li.appendChild(link);
|
|
313
|
+
outlineList.appendChild(li);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function renderPlan() {
|
|
318
|
+
if (!planEl) return;
|
|
319
|
+
closeComposer();
|
|
320
|
+
planEl.replaceChildren();
|
|
321
|
+
var blocks = Array.isArray(data.blocks) ? data.blocks : [];
|
|
322
|
+
var anchored = {};
|
|
323
|
+
blocks.forEach(function (block) {
|
|
324
|
+
if (block.kind === "blank") return;
|
|
325
|
+
anchored[block.start] = true;
|
|
326
|
+
var wrap = el("div", "bf-block");
|
|
327
|
+
wrap.setAttribute("data-line", String(block.start));
|
|
328
|
+
wrap.setAttribute("data-kind", String(block.kind));
|
|
329
|
+
if (block.kind === "heading") wrap.id = "s-" + block.start;
|
|
330
|
+
renderBlockContent(wrap, block);
|
|
331
|
+
var add = el("button", "bf-btn bf-btn--ghost bf-block__add");
|
|
332
|
+
add.type = "button";
|
|
333
|
+
add.setAttribute("aria-label", "Comment on this block");
|
|
334
|
+
add.appendChild(document.createTextNode("+"));
|
|
335
|
+
var addLabel = el("span", "bf-block__add-label");
|
|
336
|
+
addLabel.textContent = "Comment";
|
|
337
|
+
add.appendChild(addLabel);
|
|
338
|
+
add.addEventListener("click", function () {
|
|
339
|
+
var quote = pendingQuote && pendingQuote.wrap === wrap ? pendingQuote.quote : selectionIn(wrap);
|
|
340
|
+
pendingQuote = null;
|
|
341
|
+
openComposer(wrap, block, quote);
|
|
342
|
+
});
|
|
343
|
+
wrap.appendChild(add);
|
|
344
|
+
planEl.appendChild(wrap);
|
|
345
|
+
renderComments(planEl, block.start);
|
|
346
|
+
});
|
|
347
|
+
var orphans = annotations.filter(function (annotation) { return !anchored[annotation.anchor]; });
|
|
348
|
+
if (orphans.length > 0) {
|
|
349
|
+
var box = el("div", "bf-comments bf-comments--orphans");
|
|
350
|
+
var note = el("p"); note.textContent = "Comments not attached to a current block";
|
|
351
|
+
box.appendChild(note);
|
|
352
|
+
renderCommentCards(box, orphans);
|
|
353
|
+
planEl.appendChild(box);
|
|
354
|
+
}
|
|
355
|
+
observeSections();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function renderHeader() {
|
|
359
|
+
var titleEl = document.getElementById("plan-title");
|
|
360
|
+
if (titleEl && data.title) {
|
|
361
|
+
titleEl.textContent = data.title;
|
|
362
|
+
titleEl.title = data.title;
|
|
363
|
+
}
|
|
364
|
+
if (data.title) document.title = "Plan review \u00b7 " + data.title;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function lockActions() {
|
|
368
|
+
if (saveButton) saveButton.disabled = true;
|
|
369
|
+
if (cancelButton) cancelButton.disabled = true;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function save() {
|
|
373
|
+
if (saved) return;
|
|
374
|
+
setStatus("Saving\u2026");
|
|
375
|
+
fetch("/api/feedback?token=" + encodeURIComponent(token), {
|
|
376
|
+
method: "POST",
|
|
377
|
+
headers: { "Content-Type": "application/json" },
|
|
378
|
+
body: JSON.stringify({ annotations: annotations })
|
|
379
|
+
}).then(function (response) {
|
|
380
|
+
if (!response.ok) throw new Error("server returned " + response.status);
|
|
381
|
+
saved = true;
|
|
382
|
+
setStatus("Saved \u2014 return to Pi to continue.", "saved");
|
|
383
|
+
lockActions();
|
|
384
|
+
}).catch(function (error) {
|
|
385
|
+
setStatus("Could not save: " + error.message, "error");
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function cancel() {
|
|
390
|
+
if (saved || closed) return;
|
|
391
|
+
closed = true;
|
|
392
|
+
lockActions();
|
|
393
|
+
setStatus("Closing\u2026");
|
|
394
|
+
fetch("/api/cancel?token=" + encodeURIComponent(token), { method: "POST" }).catch(function () {}).then(function () {
|
|
395
|
+
setStatus("Closed without saving.");
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (saveButton) saveButton.addEventListener("click", save);
|
|
400
|
+
if (cancelButton) cancelButton.addEventListener("click", cancel);
|
|
401
|
+
|
|
402
|
+
// Remember a selection made inside a block so clicking + Comment can still
|
|
403
|
+
// quote it after the button press has collapsed the selection.
|
|
404
|
+
document.addEventListener("mouseup", function (event) {
|
|
405
|
+
if (!planEl || saved) return;
|
|
406
|
+
var wrap = event.target && event.target.closest ? event.target.closest(".bf-block") : null;
|
|
407
|
+
var quote = wrap && !wrap.querySelector(".bf-composer") ? selectionIn(wrap) : "";
|
|
408
|
+
pendingQuote = wrap && quote ? { wrap: wrap, quote: quote } : null;
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
document.addEventListener("keydown", function (event) {
|
|
412
|
+
if (event.key === "Escape") closeComposer();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
window.addEventListener("beforeunload", function () {
|
|
416
|
+
if (saved || closed) return;
|
|
417
|
+
var url = "/api/cancel?token=" + encodeURIComponent(token);
|
|
418
|
+
if (navigator.sendBeacon) navigator.sendBeacon(url);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
new EventSource("/events?token=" + encodeURIComponent(token));
|
|
423
|
+
} catch (error) {
|
|
424
|
+
// The server still sees the POST; the stream is only for close detection.
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
${THEME_TOGGLE_JS}
|
|
428
|
+
|
|
429
|
+
renderHeader();
|
|
430
|
+
renderOutline();
|
|
431
|
+
renderPlan();
|
|
432
|
+
})();
|
|
433
|
+
`;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML shell for the browser plan review (pure string building).
|
|
3
|
+
*
|
|
4
|
+
* The page is intentionally inert: it carries no plan text in its markup and
|
|
5
|
+
* no inline script. Everything the review needs travels in one JSON data
|
|
6
|
+
* island (`<script type="application/json">`), and `/client.js` renders it
|
|
7
|
+
* with DOM text nodes, so plan- and comment-authored text is never parsed as
|
|
8
|
+
* HTML. `<` is escaped in the island so a plan containing `</script>` cannot
|
|
9
|
+
* close the tag.
|
|
10
|
+
*
|
|
11
|
+
* It shares the Bytefly shell, components, and syntax colours with
|
|
12
|
+
* `file-browse` through `lib/web`; the server sets the CSP header, and the
|
|
13
|
+
* page loads only same-origin scripts.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
brandLockup,
|
|
18
|
+
docHead,
|
|
19
|
+
themeToggle,
|
|
20
|
+
WEB_BASE_CSS,
|
|
21
|
+
WEB_COMPONENTS_CSS,
|
|
22
|
+
WEB_LAYOUT_CSS,
|
|
23
|
+
WEB_THEME_CSS,
|
|
24
|
+
} from "../../lib/web/index.ts";
|
|
25
|
+
import type { PlanBlock } from "./blocks.ts";
|
|
26
|
+
import type { PlanAnnotation } from "./feedback.ts";
|
|
27
|
+
|
|
28
|
+
/** A heading the outline can jump to. */
|
|
29
|
+
export interface ReviewOutlineItem {
|
|
30
|
+
/** DOM id the client gives the heading. */
|
|
31
|
+
id: string;
|
|
32
|
+
/** 0-based source line. */
|
|
33
|
+
line: number;
|
|
34
|
+
/** Heading level, 1-6. */
|
|
35
|
+
level: number;
|
|
36
|
+
/** Heading text. */
|
|
37
|
+
text: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Everything the browser needs to render one plan review. */
|
|
41
|
+
export interface ReviewPageData {
|
|
42
|
+
/** Plan title, derived from the file name. */
|
|
43
|
+
title: string;
|
|
44
|
+
/** Source-line blocks of the feedback-stripped plan. */
|
|
45
|
+
blocks: PlanBlock[];
|
|
46
|
+
/** Feedback already stored in the file. */
|
|
47
|
+
annotations: PlanAnnotation[];
|
|
48
|
+
/** Section outline, in document order. */
|
|
49
|
+
outline: ReviewOutlineItem[];
|
|
50
|
+
/** Highlighted HTML per line, keyed by the code block's 0-based start line. */
|
|
51
|
+
code: Record<string, string[]>;
|
|
52
|
+
/** Per-session secret the client echoes on every request. */
|
|
53
|
+
token: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Page styles: the shared base + theme + shell + components, then the review. */
|
|
57
|
+
export const REVIEW_CSS = `${WEB_BASE_CSS}
|
|
58
|
+
${WEB_THEME_CSS}
|
|
59
|
+
${WEB_LAYOUT_CSS}
|
|
60
|
+
${WEB_COMPONENTS_CSS}
|
|
61
|
+
.bf-review{display:flex;flex-direction:column;padding:var(--space-5) var(--space-6) 96px}
|
|
62
|
+
.review-bar{align-items:center;gap:var(--space-2) var(--space-4)}
|
|
63
|
+
.review-bar .bf-brand{flex:0 0 auto}
|
|
64
|
+
.review-heading{display:flex;align-items:center;flex:1 1 160px;min-width:0;overflow:hidden;border-left:1px solid var(--border);padding-left:var(--space-4)}
|
|
65
|
+
.review-title{display:block;flex:1 1 auto;min-width:0;font-size:var(--fs-lg);font-weight:var(--fw-semibold);line-height:var(--lh-tight);color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
66
|
+
.review-status{flex:0 0 auto;max-width:42ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
67
|
+
.review-status:empty{display:none}
|
|
68
|
+
.review-controls{display:inline-flex;align-items:center;gap:var(--space-2);flex:0 0 auto}
|
|
69
|
+
.bf-app__body--single{grid-template-columns:minmax(0,1fr)}
|
|
70
|
+
.bf-app__sidebar .bf-panel{flex:1 1 auto;min-height:0}
|
|
71
|
+
.review-nav{min-width:0}
|
|
72
|
+
.review-nav__title{margin:0;padding:var(--space-3) var(--space-4) var(--space-2);font:var(--fw-semibold) var(--fs-sm) var(--font-mono);color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
|
|
73
|
+
.review-nav ul{list-style:none;margin:0;padding:0}
|
|
74
|
+
.review-nav a{display:block;padding:4px 8px;border-radius:var(--radius-sm);color:var(--muted);font-size:var(--fs-sm);line-height:var(--lh-snug);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
75
|
+
.review-nav a:hover{background:var(--accent-soft);color:var(--accent);text-decoration:none}
|
|
76
|
+
.review-nav a[aria-current="true"]{background:var(--accent-soft);color:var(--accent);font-weight:var(--fw-semibold)}
|
|
77
|
+
.review-nav .lvl-2 a{padding-left:18px}
|
|
78
|
+
.review-nav .lvl-3 a{padding-left:30px}
|
|
79
|
+
.review-nav .lvl-4 a,.review-nav .lvl-5 a,.review-nav .lvl-6 a{padding-left:42px}
|
|
80
|
+
.bf-review>.bf-block{position:relative;padding:2px 96px 2px 0;border-radius:var(--radius-md);overflow-wrap:break-word}
|
|
81
|
+
.bf-block[id]{scroll-margin-top:76px}
|
|
82
|
+
.bf-block:hover{background:var(--surface)}
|
|
83
|
+
.bf-block h1,.bf-block h2,.bf-block h3,.bf-block h4,.bf-block h5,.bf-block h6{margin:20px 0 8px;line-height:var(--lh-tight)}
|
|
84
|
+
.bf-block h1{font-size:24px;padding-bottom:6px;border-bottom:1px solid var(--border)}
|
|
85
|
+
.bf-block h2{font-size:19px;padding-bottom:4px;border-bottom:1px solid var(--border)}
|
|
86
|
+
.bf-block h3{font-size:16px}
|
|
87
|
+
.bf-block p{margin:10px 0}
|
|
88
|
+
.bf-block blockquote{margin:10px 0;padding:2px 14px;border-left:3px solid var(--border);color:var(--muted)}
|
|
89
|
+
.bf-block__li{display:flex;gap:8px;margin:3px 0}
|
|
90
|
+
.bf-block__marker{flex:0 0 auto;min-width:1.4em;color:var(--muted)}
|
|
91
|
+
.bf-block__marker--done{color:var(--success)}
|
|
92
|
+
.bf-block code{font-family:var(--font-mono);font-size:.9em;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:.1em .35em}
|
|
93
|
+
.bf-block pre.bf-code{margin:10px 0;padding:var(--space-3) 0;overflow:auto;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);font:var(--fs-code)/1.55 var(--font-mono);tab-size:4;flex:none}
|
|
94
|
+
.bf-block pre.bf-code code{background:none;border:0;padding:0;font-size:inherit}
|
|
95
|
+
.bf-block pre.bf-code .bf-code__line{display:block;padding:0 14px;white-space:pre;min-height:1.55em}
|
|
96
|
+
.bf-block__add{position:absolute;top:4px;right:4px;display:inline-flex;align-items:center;gap:6px;opacity:0;font-size:var(--fs-xs);padding:2px 8px}
|
|
97
|
+
.bf-block:hover .bf-block__add,.bf-block__add:focus-visible{opacity:1}
|
|
98
|
+
.bf-block--composing .bf-block__add{opacity:0}
|
|
99
|
+
.bf-comments{display:flex;flex-direction:column;gap:8px;margin:4px 0 14px;padding-left:var(--space-5)}
|
|
100
|
+
.bf-comments:empty{display:none}
|
|
101
|
+
.bf-comments--orphans{margin-top:26px;padding:var(--space-3) 0 0;border-top:1px dashed var(--border)}
|
|
102
|
+
.bf-comments--orphans p{margin:0 0 8px;color:var(--muted);font-size:var(--fs-sm)}
|
|
103
|
+
@media (max-width:1180px){
|
|
104
|
+
.bf-app__body{grid-template-columns:1fr;align-content:start}
|
|
105
|
+
.bf-app__sidebar{position:static;max-height:none;border-right:0;border-bottom:1px solid var(--border)}
|
|
106
|
+
.review-nav{padding:4px 16px 12px}
|
|
107
|
+
}
|
|
108
|
+
@media (max-width:860px){
|
|
109
|
+
.bf-review{padding:var(--space-4) 18px 80px}
|
|
110
|
+
.bf-review>.bf-block{padding-right:36px}
|
|
111
|
+
.bf-block__add-label{display:none}
|
|
112
|
+
}
|
|
113
|
+
@media (max-width:860px){
|
|
114
|
+
.review-nav__title{display:none}
|
|
115
|
+
}
|
|
116
|
+
@media (max-width:820px){
|
|
117
|
+
.review-bar{align-items:flex-start}
|
|
118
|
+
.review-heading{flex:1 1 100%;order:3;border-left:0;padding-left:0;margin-top:var(--space-1)}
|
|
119
|
+
.review-controls{order:2;margin-left:auto}
|
|
120
|
+
.review-status{order:4;flex:1 1 100%;max-width:none;white-space:normal}
|
|
121
|
+
}
|
|
122
|
+
@media (prefers-reduced-motion:no-preference){.bf-block{transition:background .08s ease}}
|
|
123
|
+
@media (hover:none){.bf-block__add{opacity:1}}
|
|
124
|
+
`.trim();
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Render the review page for `data`. The markup is a fixed shell; the client
|
|
128
|
+
* fills every dynamic field, so this function is the only place that needs the
|
|
129
|
+
* JSON-island escaping.
|
|
130
|
+
*/
|
|
131
|
+
export function renderReviewPage(data: ReviewPageData): string {
|
|
132
|
+
const island = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
133
|
+
const hasOutline = data.outline.length > 0;
|
|
134
|
+
const bodyClass = hasOutline ? "bf-app__body" : "bf-app__body bf-app__body--single";
|
|
135
|
+
const outlinePanel = hasOutline
|
|
136
|
+
? `<details class="bf-app__sidebar review-outline" open>
|
|
137
|
+
<summary class="bf-panel__summary">Sections</summary>
|
|
138
|
+
<nav class="bf-panel review-nav" aria-labelledby="outline-title"><p class="review-nav__title" id="outline-title">Sections</p><ul id="outline-list"></ul></nav>
|
|
139
|
+
</details>
|
|
140
|
+
`
|
|
141
|
+
: "";
|
|
142
|
+
return `<!doctype html>
|
|
143
|
+
<html lang="en">
|
|
144
|
+
<head>
|
|
145
|
+
${docHead({ title: "Plan review", css: REVIEW_CSS })}
|
|
146
|
+
</head>
|
|
147
|
+
<body class="bf-app">
|
|
148
|
+
<a class="skip" href="#plan">Skip to the plan</a>
|
|
149
|
+
<header class="bf-app__header"><div class="bf-toolbar review-bar">
|
|
150
|
+
${brandLockup("plan review")}
|
|
151
|
+
<div class="review-heading">
|
|
152
|
+
<span class="review-title" id="plan-title">Plan review</span>
|
|
153
|
+
</div>
|
|
154
|
+
<span class="bf-status review-status" id="status" role="status" aria-live="polite"></span>
|
|
155
|
+
<div class="review-controls">
|
|
156
|
+
${themeToggle()}
|
|
157
|
+
<button type="button" class="bf-btn bf-btn--primary" id="save">Save feedback</button>
|
|
158
|
+
<button type="button" class="bf-btn" id="cancel">Close</button>
|
|
159
|
+
</div>
|
|
160
|
+
</div></header>
|
|
161
|
+
<div class="${bodyClass}">
|
|
162
|
+
${outlinePanel}<main class="bf-app__main bf-review" id="plan" aria-label="Plan"></main>
|
|
163
|
+
</div>
|
|
164
|
+
<script type="application/json" id="review-data">${island}</script>
|
|
165
|
+
<script src="/client.js" defer></script>
|
|
166
|
+
</body>
|
|
167
|
+
</html>
|
|
168
|
+
`;
|
|
169
|
+
}
|