paseo-req-kit 1.14.0 → 1.15.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/docs/guide//347/273/264/346/212/244/350/200/205/346/214/207/345/215/227.md +2 -0
  3. package/engine/src/review-assets/app.css +153 -234
  4. package/engine/src/review-assets/app.html +44 -32
  5. package/engine/src/review-assets/app.js +11 -7
  6. package/engine/src/review-assets/check.css +11 -56
  7. package/engine/src/review-assets/check.html +48 -22
  8. package/engine/src/review-assets/check.js +169 -86
  9. package/engine/src/review-assets/code.css +15 -92
  10. package/engine/src/review-assets/code.html +41 -26
  11. package/engine/src/review-assets/code.js +196 -101
  12. package/engine/src/review-assets/rk-icons.js +98 -0
  13. package/engine/src/review-assets/rk-tokens.css +252 -0
  14. package/engine/src/review-assets/rk.css +252 -0
  15. package/engine/src/review.ts +6 -4
  16. package/package.json +1 -1
  17. package/paseo-plugin/package.json +1 -1
  18. package/site-dist/404.html +2 -2
  19. package/site-dist/assets/{app.VQ-QxRPH.js → app.D5s_yGHZ.js} +1 -1
  20. package/site-dist/assets/changelog.md.RJaBnmnw.js +1 -0
  21. package/site-dist/assets/chunks/@localSearchIndexroot.Dric-MbI.js +1 -0
  22. package/site-dist/assets/chunks/{VPLocalSearchBox.CT4Vlj76.js → VPLocalSearchBox.B4CyDsTr.js} +1 -1
  23. package/site-dist/assets/chunks/{theme.B_UmHHou.js → theme.CCocenmB.js} +2 -2
  24. package/site-dist/assets/{guide_getting-started.md.CZM53rO_.js → guide_getting-started.md.wFOkpMSx.js} +1 -1
  25. package/site-dist/assets/guide_maintainer.md.mFZprBj1.js +1 -0
  26. package/site-dist/assets/{guide_maintainer.md.C1v7EpSY.lean.js → guide_maintainer.md.mFZprBj1.lean.js} +1 -1
  27. package/site-dist/changelog.html +5 -5
  28. package/site-dist/guide/getting-started.html +5 -5
  29. package/site-dist/guide/how-it-works.html +4 -4
  30. package/site-dist/guide/index.html +4 -4
  31. package/site-dist/guide/install.html +4 -4
  32. package/site-dist/guide/maintainer.html +5 -5
  33. package/site-dist/guide/phases.html +4 -4
  34. package/site-dist/guide/project.html +4 -4
  35. package/site-dist/guide/user.html +4 -4
  36. package/site-dist/hashmap.json +1 -1
  37. package/site-dist/index.html +4 -4
  38. package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +4 -4
  39. package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +4 -4
  40. package/site-dist/reference/adr/0003-script-engine-not-hub.html +4 -4
  41. package/site-dist/reference/adr/0004-result-files.html +4 -4
  42. package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +4 -4
  43. package/site-dist/reference/adr/0006-skill-install-targets.html +4 -4
  44. package/site-dist/reference/adr/0007-own-interview-skill.html +4 -4
  45. package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +4 -4
  46. package/site-dist/reference/adr/0009-versioned-install-layout.html +4 -4
  47. package/site-dist/reference/adr/0010-generated-feature-overview.html +4 -4
  48. package/site-dist/reference/adr/0011-resume-after-completion.html +4 -4
  49. package/site-dist/reference/adr/0012-review-panel.html +4 -4
  50. package/site-dist/reference/adr/0013-no-release-tooling.html +4 -4
  51. package/site-dist/reference/adr/0014-notifier-slot.html +4 -4
  52. package/site-dist/reference/adr/0015-incremental-continue.html +4 -4
  53. package/site-dist/reference/adr/0016-upgrade-from-tags.html +4 -4
  54. package/site-dist/reference/adr/0017-use-paseo-capabilities.html +4 -4
  55. package/site-dist/reference/adr/0018-own-worktree-option.html +4 -4
  56. package/site-dist/reference/adr/0019-parallel-orchestration.html +4 -4
  57. package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +4 -4
  58. package/site-dist/reference/adr/0021-npm-version-manager.html +4 -4
  59. package/site-dist/reference/adr/0022-develop-and-verify-phases.html +4 -4
  60. package/site-dist/reference/adr/0023-types-gate-slices.html +4 -4
  61. package/site-dist/reference/adr/0024-confirm-early-review-axes.html +4 -4
  62. package/site-dist/reference/adr/0025-joint-requirements.html +4 -4
  63. package/site-dist/reference/adr/0026-machine-records-and-api.html +4 -4
  64. package/site-dist/reference/adr/0027-paseo-plugin.html +4 -4
  65. package/site-dist/reference/adr/0028-external-integrations.html +4 -4
  66. package/site-dist/reference/adr/0029-requirements-from-cards.html +4 -4
  67. package/site-dist/reference/adr/0030-card-intake-process.html +4 -4
  68. package/site-dist/reference/adr/0031-fewer-interventions.html +4 -4
  69. package/site-dist/reference/adr/0032-npm-only-install.html +4 -4
  70. package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +4 -4
  71. package/site-dist/reference/adr/index.html +4 -4
  72. package/site-dist/reference/commands.html +4 -4
  73. package/site-dist/reference/config.html +4 -4
  74. package/site-dist/reference/contracts/cli-json.html +4 -4
  75. package/site-dist/reference/contracts/feature-files.html +4 -4
  76. package/site-dist/reference/contracts/index.html +4 -4
  77. package/site-dist/reference/contracts/notifier-event.html +4 -4
  78. package/site-dist/reference/contracts/result-file.html +4 -4
  79. package/site-dist/reference/contracts/spec.html +4 -4
  80. package/site-dist/reference/contracts/state.html +4 -4
  81. package/site-dist/reference/contracts/usage-record.html +4 -4
  82. package/site-dist/reference/contracts/verify.html +4 -4
  83. package/site-dist/reference/scope.html +4 -4
  84. package/site-dist/assets/changelog.md.CIKYJXpF.js +0 -1
  85. package/site-dist/assets/chunks/@localSearchIndexroot.h7oQE_np.js +0 -1
  86. package/site-dist/assets/guide_maintainer.md.C1v7EpSY.js +0 -1
  87. /package/site-dist/assets/{changelog.md.CIKYJXpF.lean.js → changelog.md.RJaBnmnw.lean.js} +0 -0
  88. /package/site-dist/assets/{guide_getting-started.md.CZM53rO_.lean.js → guide_getting-started.md.wFOkpMSx.lean.js} +0 -0
@@ -1,16 +1,19 @@
1
- // The code review page (1.14). Plain script, no framework: the server serves it next to its JSON
2
- // API (api/info, api/draft, api/submit). Everything shown is set with textContent.
1
+ // The code review page (1.14; design system 1.15). Plain script, no framework: the server serves it
2
+ // next to its JSON API (api/info, api/draft, api/submit). Everything shown is set with textContent.
3
3
  (function () {
4
4
  "use strict";
5
5
  var state = { info: null, comments: [], decisions: {}, reviewer: "", reviewers: "", note: "", saveTimer: null, busy: false, open: null };
6
6
  var $ = function (id) {
7
7
  return document.getElementById(id);
8
8
  };
9
+ var icon = function (name, cls) {
10
+ return window.RkIcon ? window.RkIcon(name, cls) : null;
11
+ };
9
12
 
10
13
  function el(tag, attrs, children) {
11
14
  var node = document.createElement(tag);
12
15
  Object.keys(attrs || {}).forEach(function (k) {
13
- if (attrs[k] === null || attrs[k] === undefined) return;
16
+ if (attrs[k] === null || attrs[k] === undefined || attrs[k] === false) return;
14
17
  if (k === "text") node.textContent = attrs[k];
15
18
  else if (k === "className") node.className = attrs[k];
16
19
  else if (k.indexOf("on") === 0) node.addEventListener(k.slice(2), attrs[k]);
@@ -22,6 +25,10 @@
22
25
  return node;
23
26
  }
24
27
 
28
+ function badge(kind, iconName, text) {
29
+ return el("span", { className: "rk-badge rk-badge--" + kind }, [iconName ? icon(iconName) : null, text]);
30
+ }
31
+
25
32
  function toast(text) {
26
33
  var t = $("toast");
27
34
  t.textContent = text;
@@ -50,11 +57,22 @@
50
57
  .filter(Boolean);
51
58
  }
52
59
 
60
+ function hhmm() {
61
+ var d = new Date();
62
+ return (d.getHours() < 10 ? "0" : "") + d.getHours() + ":" + (d.getMinutes() < 10 ? "0" : "") + d.getMinutes();
63
+ }
64
+
53
65
  function save() {
54
66
  if (state.info.readOnly) return;
55
67
  clearTimeout(state.saveTimer);
56
68
  state.saveTimer = setTimeout(function () {
57
- api("api/draft", "PUT", { comments: state.comments, decisions: state.decisions, reviewer: state.reviewer, reviewers: state.reviewers, note: state.note }).catch(function () {});
69
+ api("api/draft", "PUT", { comments: state.comments, decisions: state.decisions, reviewer: state.reviewer, reviewers: state.reviewers, note: state.note })
70
+ .then(function () {
71
+ $("saved").textContent = "已自动保存 " + hhmm();
72
+ })
73
+ .catch(function () {
74
+ $("saved").textContent = "没保存上,网络恢复后再改一下会重试";
75
+ });
58
76
  }, 600);
59
77
  count();
60
78
  }
@@ -75,19 +93,28 @@
75
93
  }).length;
76
94
  var fix = fixes().length;
77
95
  var total = comments + fix + (state.note.trim() ? 1 : 0);
78
- $("count").textContent = "代码行上的意见 " + comments + " 条,AI 意见标为要改 " + fix + " 条";
96
+ $("count").textContent = total ? total + " 条修改意见" : "还没有修改意见";
97
+ $("countsub").textContent = state.info.readOnly
98
+ ? "这是只读链接,只能查看"
99
+ : state.info.running
100
+ ? "流水线正在运行,结束后才能提交评审"
101
+ : "代码行上的意见 " + comments + " 条,AI 意见标为要改 " + fix + " 条";
79
102
  var blocked = state.info.readOnly || state.info.running || state.busy;
80
103
  $("changes").disabled = blocked || total === 0;
81
- $("changes").textContent = total ? "退回修改(" + total + " 条意见)" : "退回修改";
104
+ $("changes").title = total === 0 ? "写一条意见,或把 AI 意见标为要改,才能退回修改" : "";
105
+ $("changes").lastChild.textContent = total ? "退回修改(" + total + " 条)" : "退回修改";
82
106
  $("approve").disabled = blocked;
83
107
  }
84
108
 
85
- var SEVERITY_CLASS = { "阻塞": "danger", "重要": "warn", "建议": "", "待核实": "" };
86
- var STATUS_TEXT = { fixed: "已修复", rejected: "修复时判为不成立", wontfix: "决定不修复", open: "未处理" };
109
+ var SEVERITY = { "阻塞": ["failed", "octagon-alert"], "重要": ["warning", "triangle-alert"], "建议": ["muted", null], "待核实": ["ai", "circle-dashed"] };
110
+ var STATUS = { fixed: ["success", "circle-check", "已修复"], rejected: ["outline", null, "修复时判为不成立"], wontfix: ["outline", null, "决定不修复"], open: ["outline", null, "还开着"] };
87
111
 
88
- function findingNode(f, compact) {
112
+ function findingNode(f, inline) {
89
113
  var d = state.decisions[f.id] || {};
90
- var note = el("input", { className: "note-input", placeholder: "补充一句(可不填)", value: d.note || "", hidden: d.mark === "fix" ? null : "" });
114
+ var sev = SEVERITY[f.severity] || ["muted", null];
115
+ var st = STATUS[f.status] || STATUS.open;
116
+ var note = el("input", { className: "rk-input", placeholder: "补充一句(可不填)", value: d.note || "", "aria-label": "补充说明", hidden: d.mark === "fix" ? null : "" });
117
+ note.style.flex = "1 1 200px";
91
118
  note.addEventListener("input", function (e) {
92
119
  state.decisions[f.id] = { mark: "fix", note: e.target.value };
93
120
  save();
@@ -102,43 +129,47 @@
102
129
  };
103
130
  }
104
131
  var where = f.file ? f.file + (f.line ? ":" + f.line : "") : "";
105
- return el("div", { className: "finding " + (SEVERITY_CLASS[f.severity] || "") + (d.mark ? " decided-" + d.mark : "") }, [
106
- el("div", { className: "finding-head" }, [
107
- el("span", { className: "badge " + (SEVERITY_CLASS[f.severity] || ""), text: "AI · " + f.severity }),
108
- el("b", { text: f.id }),
109
- f.status !== "open" ? el("span", { className: "badge " + (f.status === "fixed" ? "ok" : ""), text: STATUS_TEXT[f.status] }) : null,
110
- !compact && where ? el("a", { href: "#", className: "where", text: where, onclick: function (e) {
111
- e.preventDefault();
112
- jump(f.file, f.line);
113
- } }) : null,
132
+ return el("div", { className: "rk-finding rk-finding--ai" + (d.mark === "ok" ? " is-ok" : "") }, [
133
+ el("div", { className: "rk-finding__head" }, [
134
+ el("span", { className: "rk-finding__who" }, [icon("sparkles", "ic ic-sm"), "AI 评审"]),
135
+ el("span", { className: "rk-finding__id", text: f.id + (f.round ? " · 第 " + f.round + " 轮" : "") }),
136
+ badge(sev[0], sev[1], f.severity),
137
+ badge(st[0], st[1], st[2]),
138
+ !inline && where
139
+ ? el("a", { href: "#", className: "rk-finding__where", text: where, onclick: function (e) {
140
+ e.preventDefault();
141
+ jump(f.file, f.line);
142
+ } })
143
+ : null,
114
144
  ]),
115
- el("div", { className: "finding-text", text: f.title }),
116
- f.suggestion ? el("div", { className: "muted small", text: "建议:" + f.suggestion }) : null,
145
+ el("div", { className: "rk-finding__body", text: f.title }),
146
+ f.suggestion ? el("div", { className: "rk-finding__fix", text: "建议:" + f.suggestion }) : null,
117
147
  state.info.readOnly
118
148
  ? null
119
- : el("div", { className: "marks" }, [
120
- el("button", { className: "btn small" + (d.mark === "fix" ? " on-fix" : ""), text: "要改", onclick: mark("fix") }),
121
- el("button", { className: "btn small" + (d.mark === "ok" ? " on-ok" : ""), text: "不用改", onclick: mark("ok") }),
149
+ : el("div", { className: "rk-finding__acts" }, [
150
+ el("div", { className: "rk-seg", role: "group", "aria-label": "这条意见要不要改" }, [
151
+ el("button", { type: "button", className: "is-fix", "aria-pressed": String(d.mark === "fix"), text: "要改", onclick: mark("fix") }),
152
+ el("button", { type: "button", className: "is-ok", "aria-pressed": String(d.mark === "ok"), text: "不用改", onclick: mark("ok") }),
153
+ ]),
154
+ note,
122
155
  ]),
123
- note,
124
156
  ]);
125
157
  }
126
158
 
127
159
  function jump(file, line) {
128
160
  var target = document.querySelector('[data-file="' + CSS.escape(file) + '"]' + (line ? '[data-n="' + line + '"]' : ""));
129
161
  if (!target) target = document.querySelector('[data-file-head="' + CSS.escape(file) + '"]');
130
- if (target) {
131
- var box = target.closest(".file");
132
- if (box && box.classList.contains("folded")) box.classList.remove("folded");
133
- target.scrollIntoView({ block: "center", behavior: "smooth" });
134
- target.classList.add("flash");
135
- setTimeout(function () {
136
- target.classList.remove("flash");
137
- }, 1600);
138
- }
162
+ if (!target) return;
163
+ var box = target.closest(".rk-diff");
164
+ if (box && box.classList.contains("folded")) box.classList.remove("folded");
165
+ target.scrollIntoView({ block: "center", behavior: "smooth" });
166
+ target.classList.add("rk-flash");
167
+ setTimeout(function () {
168
+ target.classList.remove("rk-flash");
169
+ }, 1600);
139
170
  }
140
171
 
141
- function commentBox(file, line) {
172
+ function commentRow(file, line) {
142
173
  var existing = state.comments.find(function (c) {
143
174
  return c.file === file && c.line === line;
144
175
  });
@@ -146,38 +177,58 @@
146
177
  existing = { file: file, line: line, text: "" };
147
178
  state.comments.push(existing);
148
179
  }
149
- var ta = el("textarea", { rows: "2", placeholder: "这一行哪里不对、应该怎么改" });
180
+ var ta = el("textarea", { className: "rk-input", rows: "2", placeholder: "这一行哪里不对、应该怎么改", "aria-label": "你的意见" });
150
181
  ta.value = existing.text;
182
+ ta.disabled = !!state.info.readOnly;
151
183
  ta.addEventListener("input", function (e) {
152
184
  existing.text = e.target.value;
153
185
  save();
154
186
  });
155
- var remove = el("button", { className: "btn small", text: "删除", onclick: function () {
187
+ var remove = el("button", { type: "button", className: "rk-btn rk-btn--ghost rk-btn--sm", onclick: function () {
156
188
  state.comments = state.comments.filter(function (c) {
157
189
  return c !== existing;
158
190
  });
159
191
  state.open = null;
160
192
  render();
161
193
  save();
162
- } });
163
- return el("tr", { className: "comment-row" }, [el("td", { colspan: "3" }, [el("div", { className: "comment" }, [el("div", { className: "muted small", text: "你的意见(" + file + ":" + line + ")" }), ta, state.info.readOnly ? null : remove])])]);
194
+ } }, [icon("trash-2", "ic ic-sm"), "删除"]);
195
+ var card = el("div", { className: "rk-finding rk-finding--you" }, [
196
+ el("div", { className: "rk-finding__head" }, [el("span", { className: "rk-finding__who" }, [icon("message-square-plus", "ic ic-sm"), "你的意见"]), el("span", { className: "rk-finding__where", text: file + ":" + line }), state.info.readOnly ? null : remove]),
197
+ ta,
198
+ ]);
199
+ if (state.open === file + ":" + line && !existing.text) setTimeout(function () {
200
+ ta.focus();
201
+ });
202
+ return el("tr", { className: "note" }, [el("td", { colspan: "3" }, [card])]);
164
203
  }
165
204
 
205
+ var FILE_STATUS = { added: "新增", deleted: "删除", modified: "修改", renamed: "改名" };
206
+
166
207
  function renderFile(f, findingsByFile) {
167
208
  var mine = findingsByFile[f.path] || [];
168
209
  var owners = (state.info.reviewers.byFile || {})[f.path] || [];
169
- var head = el("div", { className: "file-head", "data-file-head": f.path, onclick: function () {
170
- box.classList.toggle("folded");
171
- } }, [
172
- el("span", { className: "path", text: f.path + (f.oldPath ? "(原 " + f.oldPath + ")" : "") }),
173
- el("span", { className: "badge", text: { added: "新增", deleted: "删除", modified: "修改", renamed: "改名" }[f.status] }),
174
- el("span", { className: "plus", text: "+" + f.additions }),
175
- el("span", { className: "minus", text: "−" + f.deletions }),
176
- f.doc ? el("span", { className: "badge", text: "需求文档" }) : null,
177
- mine.length ? el("span", { className: "badge warn", text: "AI 意见 " + mine.length }) : null,
178
- owners.length ? el("span", { className: "muted small", text: "负责人 " + owners.join(" ") }) : null,
210
+ var box;
211
+ var head = el("div", { className: "rk-diff__head", role: "button", tabindex: "0", "aria-label": "展开或折叠 " + f.path, "data-file-head": f.path }, [
212
+ icon("chevron-down"),
213
+ el("span", { className: "rk-diff__path", text: f.path + (f.oldPath ? "(原 " + f.oldPath + ")" : "") }),
214
+ el("span", { className: "rk-diff__n" }, [el("span", { className: "a", text: "+" + f.additions }), " ", el("span", { className: "r", text: "−" + f.deletions })]),
215
+ badge("muted", null, FILE_STATUS[f.status] || f.status),
216
+ f.doc ? badge("muted", "file-text", "需求文档") : null,
217
+ mine.length ? badge("ai", "sparkles", "AI 意见 " + mine.length) : null,
218
+ el("span", { className: "rk-top__spacer" }),
219
+ owners.length ? el("span", { className: "rk-cap", text: "审核人 " + owners.join(" ") }) : null,
179
220
  ]);
180
- var body = el("div", { className: "file-body" });
221
+ function toggle() {
222
+ box.classList.toggle("folded");
223
+ }
224
+ head.addEventListener("click", toggle);
225
+ head.addEventListener("keydown", function (e) {
226
+ if (e.key === "Enter" || e.key === " ") {
227
+ e.preventDefault();
228
+ toggle();
229
+ }
230
+ });
231
+ var body = el("div", { className: "rk-diff__body" });
181
232
  var unplaced = mine.filter(function (x) {
182
233
  return !x.line || !f.hunks.some(function (h) {
183
234
  return h.lines.some(function (l) {
@@ -185,19 +236,23 @@
185
236
  });
186
237
  });
187
238
  });
188
- unplaced.forEach(function (x) {
189
- body.appendChild(findingNode(x, true));
190
- });
191
- if (f.binary) body.appendChild(el("div", { className: "muted pad", text: "二进制文件,不显示内容。" }));
239
+ if (unplaced.length) {
240
+ var wrap = el("div", { className: "rk-col", style: "padding:12px 16px" });
241
+ unplaced.forEach(function (x) {
242
+ wrap.appendChild(findingNode(x, true));
243
+ });
244
+ body.appendChild(wrap);
245
+ }
246
+ if (f.binary) body.appendChild(el("div", { className: "rk-cap", style: "padding:12px 16px", text: "二进制文件,不显示内容。" }));
192
247
  f.hunks.forEach(function (h) {
193
- var table = el("table", { className: "diff" });
194
- table.appendChild(el("tr", { className: "hunk" }, [el("td", { colspan: "3", text: h.header })]));
248
+ var table = el("table");
249
+ table.appendChild(el("tr", { className: "hunk" }, [el("td", { className: "n" }), el("td", { className: "n" }), el("td", { text: h.header })]));
195
250
  h.lines.forEach(function (l) {
196
251
  var line = l.n || l.o;
197
- var row = el("tr", { className: "line " + (l.t === "+" ? "add" : l.t === "-" ? "del" : ""), "data-file": f.path, "data-n": l.n || "", title: state.info.readOnly ? "" : "点这一行写意见" }, [
198
- el("td", { className: "num", text: l.o === null ? "" : String(l.o) }),
199
- el("td", { className: "num", text: l.n === null ? "" : String(l.n) }),
200
- el("td", { className: "code", text: (l.t === " " ? " " : l.t) + " " + l.s }),
252
+ var row = el("tr", { className: "line " + (l.t === "+" ? "add" : l.t === "-" ? "del" : "ctx"), "data-file": f.path, "data-n": l.n || "", title: state.info.readOnly ? "" : "点这一行写意见" }, [
253
+ el("td", { className: "n", text: l.o === null ? "" : String(l.o) }),
254
+ el("td", { className: "n", text: l.n === null ? "" : String(l.n) }),
255
+ el("td", { className: "s", text: l.s }),
201
256
  ]);
202
257
  if (!state.info.readOnly) {
203
258
  row.addEventListener("click", function () {
@@ -211,20 +266,27 @@
211
266
  return x.line && l.n === x.line;
212
267
  })
213
268
  .forEach(function (x) {
214
- table.appendChild(el("tr", { className: "finding-row" }, [el("td", { colspan: "3" }, [findingNode(x, true)])]));
269
+ table.appendChild(el("tr", { className: "note" }, [el("td", { colspan: "3" }, [findingNode(x, true)])]));
215
270
  });
216
271
  var hasComment = state.comments.some(function (c) {
217
272
  return c.file === f.path && c.line === line;
218
273
  });
219
- if (hasComment || state.open === f.path + ":" + line) table.appendChild(commentBox(f.path, line));
274
+ if (hasComment || state.open === f.path + ":" + line) table.appendChild(commentRow(f.path, line));
220
275
  });
221
276
  body.appendChild(table);
222
277
  });
223
- if (f.truncated) body.appendChild(el("div", { className: "muted pad", text: "改动太多,只显示了一部分。完整改动请在开发工具里看。" }));
224
- var box = el("section", { className: "file" + (f.doc && !mine.length ? " folded" : ""), id: "file-" + f.path }, [head, body]);
278
+ if (f.truncated) body.appendChild(el("div", { className: "rk-cap", style: "padding:12px 16px", text: "改动太多,只显示了一部分。完整改动请在开发工具里看。" }));
279
+ box = el("section", { className: "rk-diff" + (f.doc && !mine.length ? " folded" : ""), id: "file-" + f.path }, [head, body]);
225
280
  return box;
226
281
  }
227
282
 
283
+ function navLink(label, iconName, n, cls, onclick) {
284
+ return el("a", { href: "#", className: cls || "", onclick: function (e) {
285
+ e.preventDefault();
286
+ onclick();
287
+ } }, [icon(iconName), el("span", { text: label }), n ? el("span", { className: "cr-files-n" + (/^AI/.test(n) ? " has-ai" : ""), text: n }) : null]);
288
+ }
289
+
228
290
  function render() {
229
291
  var info = state.info;
230
292
  var byFile = {};
@@ -234,48 +296,74 @@
234
296
  })) (byFile[x.file] = byFile[x.file] || []).push(x);
235
297
  });
236
298
  var folded = {};
237
- document.querySelectorAll(".file").forEach(function (n) {
299
+ document.querySelectorAll("#diffs .rk-diff").forEach(function (n) {
238
300
  folded[n.id] = n.classList.contains("folded");
239
301
  });
240
302
  var diffs = $("diffs");
241
303
  var y = window.scrollY;
242
304
  diffs.textContent = "";
243
- if (!info.files.length) diffs.appendChild(el("div", { className: "card muted", text: info.base.commit ? "没有找到改动。" : "找不到这个需求是从哪个提交开始的,没法列出改动。" }));
305
+ if (!info.files.length) {
306
+ diffs.appendChild(el("div", { className: "rk-card rk-card--sm" }, [el("div", { className: "rk-empty" }, [icon("git-branch", "ic ic-xl"), el("h3", { text: info.base.commit ? "没有改动" : "找不到改动的起点" }), el("p", { text: info.base.commit ? "这个需求的分支和它分出来的地方一样。" : "找不到这个需求是从哪个提交开始的,没法列出改动。" })])]));
307
+ }
244
308
  info.files.forEach(function (f) {
245
309
  var node = renderFile(f, byFile);
246
310
  if (node.id in folded) node.classList.toggle("folded", folded[node.id]);
247
311
  diffs.appendChild(node);
248
312
  });
249
- // The AI findings not on a changed file: listed at the top.
313
+
314
+ // AI findings: a summary card; the ones not on a changed line, and the serious ones, listed here too.
250
315
  var ai = $("ai");
251
316
  ai.textContent = "";
252
317
  var elsewhere = info.findings.filter(function (x) {
253
318
  return !byFile[x.file] || byFile[x.file].indexOf(x) < 0;
254
319
  });
255
- if (info.findings.length) {
320
+ if (info.findings.length || info.rounds.length) {
256
321
  ai.hidden = false;
257
322
  var open = info.findings.filter(function (x) {
258
323
  return x.status === "open";
259
324
  }).length;
260
- ai.appendChild(el("h2", { text: "AI 评审意见(" + info.findings.length + " 条,未处理 " + open + " 条)" }));
261
- ai.appendChild(el("p", { className: "muted small", text: "在代码旁边也能看到。阻塞、重要的未处理意见建议标为要改;拿不准的请开发同事看一下。" }));
325
+ var fixed = info.findings.filter(function (x) {
326
+ return x.status === "fixed";
327
+ }).length;
328
+ var last = info.rounds.length ? info.rounds[info.rounds.length - 1] : null;
329
+ ai.appendChild(
330
+ el("div", { className: "rk-row", style: "justify-content:space-between" }, [
331
+ el("span", { className: "rk-card__label rk-card__label--ai" }, [icon("sparkles", "ic ic-sm"), "AI 评审 · " + info.rounds.length + " 轮"]),
332
+ el("span", { className: "rk-row", style: "gap:8px" }, [fixed ? badge("success", "circle-check", "已修复 " + fixed) : null, open ? badge("outline", null, "还开着 " + open) : null, last ? badge(last.verdict === "pass" ? "ai" : "warning", last.verdict === "pass" ? "sparkles" : "triangle-alert", last.verdict === "pass" ? "最后一轮:可以合并" : "最后一轮:需要修改") : null]),
333
+ ]),
334
+ );
335
+ ai.appendChild(el("p", { text: info.findings.length ? "意见放在下面对应的代码行旁边。阻塞、重要的未处理意见建议标为要改;拿不准的请开发同事看一下。" : "AI 评审没有留下结构化的意见,详细内容见 code-review.md。" }));
262
336
  info.findings.forEach(function (x) {
263
- if (elsewhere.indexOf(x) >= 0 || x.severity === "阻塞" || x.severity === "重要") ai.appendChild(findingNode(x, false));
337
+ if (elsewhere.indexOf(x) >= 0 || ((x.severity === "阻塞" || x.severity === "重要") && x.status === "open")) ai.appendChild(findingNode(x, false));
264
338
  });
265
- } else if (info.rounds.length) {
266
- ai.hidden = false;
267
- ai.appendChild(el("h2", { text: "AI 评审" }));
268
- ai.appendChild(el("p", { className: "muted", text: "AI 评审没有留下结构化的意见,详细内容见 code-review.md。" }));
269
339
  }
340
+
270
341
  var nav = $("files");
271
342
  nav.textContent = "";
272
- nav.appendChild(el("div", { className: "nav-title", text: "改动的文件(" + info.files.length + ")" }));
273
- info.files.forEach(function (f) {
274
- nav.appendChild(el("a", { href: "#", className: f.doc ? "doc" : "", onclick: function (e) {
275
- e.preventDefault();
276
- jump(f.path, null);
277
- } }, [el("span", { text: f.path }), (byFile[f.path] || []).length ? el("span", { className: "badge warn", text: String(byFile[f.path].length) }) : null]));
343
+ nav.appendChild(el("div", { className: "rk-side__group", text: "概要" }));
344
+ if (!$("summary").hidden) nav.appendChild(navLink("这次改了什么", "list-checks", null, "", function () {
345
+ $("summary").scrollIntoView({ block: "start", behavior: "smooth" });
346
+ }));
347
+ if (!ai.hidden) nav.appendChild(navLink("AI 评审意见", "sparkles", info.findings.length ? String(info.findings.length) : null, "", function () {
348
+ ai.scrollIntoView({ block: "start", behavior: "smooth" });
349
+ }));
350
+ var code = info.files.filter(function (f) {
351
+ return !f.doc;
352
+ });
353
+ var docs = info.files.filter(function (f) {
354
+ return f.doc;
355
+ });
356
+ [["代码 · " + code.length + " 个文件", code, "file-code"], ["需求文档 · " + docs.length + " 个", docs, "file-text"]].forEach(function (g) {
357
+ if (!g[1].length) return;
358
+ nav.appendChild(el("div", { className: "rk-side__group", text: g[0] }));
359
+ g[1].forEach(function (f) {
360
+ var n = (byFile[f.path] || []).length;
361
+ nav.appendChild(navLink(f.doc ? f.path.split("/").pop() : f.path.split("/").slice(-2).join("/"), g[2], n ? "AI " + n : "+" + f.additions, f.doc ? "is-doc" : "", function () {
362
+ jump(f.path, null);
363
+ }));
364
+ });
278
365
  });
366
+ window.RkFill && window.RkFill();
279
367
  window.scrollTo(0, y);
280
368
  count();
281
369
  }
@@ -309,11 +397,23 @@
309
397
  });
310
398
  }
311
399
 
400
+ function setBanner(kind, iconName, text) {
401
+ var banner = $("status");
402
+ banner.hidden = false;
403
+ banner.className = "rk-banner rk-banner--" + kind;
404
+ banner.textContent = "";
405
+ banner.appendChild(icon(iconName));
406
+ banner.appendChild(el("span", { text: text }));
407
+ }
408
+
312
409
  function load() {
313
410
  return api("api/info").then(function (info) {
314
411
  state.info = info;
412
+ document.title = "代码审查 · " + info.title;
413
+ $("title").textContent = info.title;
315
414
  $("project").textContent = info.featureDir;
316
- $("title").textContent = "代码审查:" + info.title;
415
+ $("project").hidden = false;
416
+ $("readonly").hidden = !info.readOnly;
317
417
  var adds = 0,
318
418
  dels = 0;
319
419
  info.files.forEach(function (f) {
@@ -322,39 +422,34 @@
322
422
  dels += f.deletions;
323
423
  }
324
424
  });
325
- var last = info.rounds.length ? info.rounds[info.rounds.length - 1] : null;
326
425
  $("sub").textContent =
327
426
  (info.branch ? "分支 " + info.branch : "") +
328
427
  (info.base.ref ? ",相对 " + info.base.ref : "") +
329
- ":改了 " + info.files.filter(function (f) {
428
+ ":改了 " +
429
+ info.files.filter(function (f) {
330
430
  return !f.doc;
331
- }).length + " 个代码文件(+" + adds + " −" + dels + ")" +
332
- (last ? ";AI 评审 " + info.rounds.length + " 轮,最后一轮" + (last.verdict === "pass" ? "通过" : "需要修改") : "");
333
- var banner = $("status");
334
- banner.hidden = true;
335
- banner.className = "banner";
431
+ }).length +
432
+ " 个代码文件(+" + adds + " −" + dels + ")。";
433
+ $("status").hidden = true;
336
434
  if (info.running) {
337
- banner.hidden = false;
338
- banner.textContent = "流水线正在运行,改动还会变化。可以先看,结束后再提交评审。";
435
+ setBanner("run", "loader-circle", "流水线正在运行,改动还会变化。可以先看,结束后再提交评审。");
339
436
  } else if (info.human) {
340
- banner.hidden = false;
341
437
  var h = info.human;
342
438
  if (h.status === "approved") {
343
- banner.className = "banner " + (h.stale ? "warn" : "ok");
344
- banner.textContent = (h.stale ? "评审通过后代码又有改动,需要重新评审。" : "已评审通过") + "(" + h.reviewer + "," + h.at.slice(0, 10) + (h.reviewers.length ? ",审核人 " + h.reviewers.join(" ") : "") + ")";
439
+ setBanner(h.stale ? "warn" : "ok", h.stale ? "triangle-alert" : "circle-check", (h.stale ? "评审通过后代码又有改动,需要重新评审。" : "已评审通过") + "(" + h.reviewer + "," + h.at.slice(0, 10) + (h.reviewers.length ? ",审核人 " + h.reviewers.join(" ") : "") + ")");
345
440
  } else {
346
- banner.className = "banner warn";
347
- banner.textContent = "上次退回了 " + h.items.length + " 条意见(" + h.reviewer + "," + h.at.slice(0, 10) + ")" + (h.consumedAttempt ? ",第 " + h.consumedAttempt + " 次尝试在处理" : ",还没开始修改");
441
+ setBanner("warn", "undo-2", "上次退回了 " + h.items.length + " 条意见(" + h.reviewer + "," + h.at.slice(0, 10) + ")" + (h.consumedAttempt ? ",第 " + h.consumedAttempt + " 次尝试在处理" : ",还没开始修改"));
348
442
  }
349
443
  }
350
444
  var sum = $("summary");
351
445
  sum.textContent = "";
446
+ sum.hidden = !info.summary.length;
352
447
  if (info.summary.length) {
353
- sum.hidden = false;
354
- sum.appendChild(el("h2", { text: "这次改了什么(来自提测说明)" }));
355
- info.summary.forEach(function (l) {
356
- sum.appendChild(el("div", { text: l.replace(/^\s*[-*]\s+/, "• ") }));
357
- });
448
+ sum.appendChild(el("span", { className: "rk-card__label" }, [icon("list-checks", "ic ic-sm"), "提测说明"]));
449
+ sum.appendChild(el("h3", { style: "font-size:17px;line-height:26px", text: "这次改了什么" }));
450
+ sum.appendChild(el("ul", { className: "cr-summary" }, info.summary.map(function (l) {
451
+ return el("li", { text: l.replace(/^\s*[-*]\s+/, "") });
452
+ })));
358
453
  }
359
454
  var d = info.draft || {};
360
455
  state.comments = Array.isArray(d.comments) ? d.comments : [];
@@ -0,0 +1,98 @@
1
+ /* Copied from design/examples/icons.js by scripts/tokens-build.mjs. Do not edit here. */
2
+ // Lucide icons (stroke 1.75 via the .ic class) used by the req-kit examples: <svg class="ic" data-i="sparkles"></svg>.
3
+ // Inline so the pages open offline (the review pages load a copy as rk-icons.js; see scripts/tokens-build.mjs).
4
+ (() => {
5
+ const I = {
6
+ check: '<path d="M20 6 9 17l-5-5"/>',
7
+ sparkles: '<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/>',
8
+ "circle-check": '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
9
+ "circle-x": '<circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>',
10
+ "circle-dashed": '<path d="M10.1 2.182a10 10 0 0 1 3.8 0"/><path d="M13.9 21.818a10 10 0 0 1-3.8 0"/><path d="M17.609 3.721a10 10 0 0 1 2.69 2.7"/><path d="M2.182 13.9a10 10 0 0 1 0-3.8"/><path d="M20.279 17.609a10 10 0 0 1-2.7 2.69"/><path d="M21.818 10.1a10 10 0 0 1 0 3.8"/><path d="M3.721 6.391a10 10 0 0 1 2.7-2.69"/><path d="M6.391 20.279a10 10 0 0 1-2.69-2.7"/>',
11
+ "circle-dot": '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="1"/>',
12
+ "loader-circle": '<path d="M21 12a9 9 0 1 1-6.219-8.56"/>',
13
+ "file-text": '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>',
14
+ "file-code": '<path d="M10 12.5 8 15l2 2.5"/><path d="m14 12.5 2 2.5-2 2.5"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/>',
15
+ "messages-square": '<path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2z"/><path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1"/>',
16
+ "message-square-plus": '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M12 7v6"/><path d="M9 10h6"/>',
17
+ "pen-tool": '<path d="M15.707 21.293a1 1 0 0 1-1.414 0l-1.586-1.586a1 1 0 0 1 0-1.414l5.586-5.586a1 1 0 0 1 1.414 0l1.586 1.586a1 1 0 0 1 0 1.414z"/><path d="m18 13-1.375-6.874a1 1 0 0 0-.746-.776L3.235 2.028a1 1 0 0 0-1.207 1.207L5.35 15.879a1 1 0 0 0 .776.746L13 18"/><path d="m2.3 2.3 7.286 7.286"/><circle cx="11" cy="11" r="2"/>',
18
+ workflow: '<rect width="8" height="8" x="3" y="3" rx="2"/><path d="M7 11v4a2 2 0 0 0 2 2h4"/><rect width="8" height="8" x="13" y="13" rx="2"/>',
19
+ "shield-check": '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/>',
20
+ "git-pull-request": '<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><line x1="6" x2="6" y1="9" y2="21"/>',
21
+ "git-branch": '<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
22
+ "clipboard-check": '<rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="m9 14 2 2 4-4"/>',
23
+ play: '<polygon points="6 3 20 12 6 21 6 3"/>',
24
+ inbox: '<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>',
25
+ "undo-2": '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"/>',
26
+ "octagon-alert": '<path d="M12 16h.01"/><path d="M12 8v4"/><path d="M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z"/>',
27
+ "triangle-alert": '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
28
+ "chart-column": '<path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/>',
29
+ "chevron-down": '<path d="m6 9 6 6 6-6"/>',
30
+ "chevron-right": '<path d="m9 18 6-6-6-6"/>',
31
+ "arrow-right": '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
32
+ copy: '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
33
+ sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
34
+ lightbulb: '<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>',
35
+ info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
36
+ clock: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
37
+ user: '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
38
+ users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
39
+ search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
40
+ terminal: '<polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/>',
41
+ lock: '<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
42
+ "list-checks": '<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/>',
43
+ "mouse-pointer-click": '<path d="M14 4.1 12 6"/><path d="m5.1 8-2.9-.8"/><path d="m6 12-1.9 2"/><path d="M7.2 2.2 8 5.1"/><path d="M9.037 9.69a.498.498 0 0 1 .653-.653l11 4.5a.5.5 0 0 1-.074.949l-4.349 1.041a1 1 0 0 0-.74.739l-1.04 4.35a.5.5 0 0 1-.95.074z"/>',
44
+ history: '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
45
+ "trash-2": '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/>',
46
+ moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
47
+ "external-link": '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
48
+ folder: '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
49
+ };
50
+ // The req-kit mark: <svg data-logo></svg> (brand-edge: lime on dark grounds, the deeper lime on light ones; the hollow nodes take --rk-canvas or the element's --ground).
51
+ const LOGO = '<path d="M16 54 L40 77 L82 30" fill="none" stroke="var(--rk-brand-edge)" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/><g fill="var(--ground, var(--rk-canvas))" stroke="var(--rk-brand-edge)" stroke-width="6"><circle cx="16" cy="54" r="7"/><circle cx="40" cy="77" r="7"/></g><circle cx="82" cy="30" r="12" fill="var(--rk-brand-edge)"/><circle cx="82" cy="30" r="19" fill="none" stroke="var(--rk-brand-edge)" stroke-width="2" opacity=".45"/>';
52
+ const fill = () => {
53
+ for (const el of document.querySelectorAll("svg[data-logo]")) {
54
+ el.setAttribute("viewBox", "0 0 100 100");
55
+ el.setAttribute("aria-hidden", "true");
56
+ el.innerHTML = LOGO;
57
+ }
58
+ for (const el of document.querySelectorAll("svg[data-i]")) {
59
+ const p = I[el.dataset.i];
60
+ if (!p) continue;
61
+ el.setAttribute("viewBox", "0 0 24 24");
62
+ el.setAttribute("aria-hidden", "true");
63
+ el.innerHTML = p;
64
+ }
65
+ };
66
+ // For pages that build their DOM in script: RkIcon("sparkles", "ic ic-sm") returns an <svg>.
67
+ window.RkIcon = (name, cls) => {
68
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
69
+ svg.setAttribute("class", cls || "ic");
70
+ svg.setAttribute("viewBox", "0 0 24 24");
71
+ svg.setAttribute("aria-hidden", "true");
72
+ svg.innerHTML = I[name] || "";
73
+ return svg;
74
+ };
75
+ window.RkFill = fill;
76
+ // A button with data-rk-theme switches between dark and light and remembers the choice.
77
+ const KEY = "rk-theme";
78
+ try {
79
+ const saved = localStorage.getItem(KEY);
80
+ if (saved === "dark" || saved === "light") document.documentElement.dataset.theme = saved;
81
+ } catch {
82
+ /* no storage: follow the system */
83
+ }
84
+ document.addEventListener("click", (e) => {
85
+ const b = e.target.closest && e.target.closest("[data-rk-theme]");
86
+ if (!b) return;
87
+ const r = document.documentElement;
88
+ const dark = r.dataset.theme ? r.dataset.theme === "dark" : !matchMedia("(prefers-color-scheme: light)").matches;
89
+ r.dataset.theme = dark ? "light" : "dark";
90
+ try {
91
+ localStorage.setItem(KEY, r.dataset.theme);
92
+ } catch {
93
+ /* not remembered */
94
+ }
95
+ });
96
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", fill);
97
+ else fill();
98
+ })();