jekyll-theme-zer0 1.24.0 → 1.26.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +124 -0
- data/README.md +4 -4
- data/_data/authors.yml +1 -0
- data/_data/backlog.yml +26 -1
- data/_data/features.yml +73 -45
- data/_data/feedback_types.yml +148 -0
- data/_data/navigation/README.md +16 -6
- data/_data/navigation/main.yml +0 -8
- data/_data/roadmap.yml +1 -0
- data/_includes/README.md +10 -7
- data/_includes/analytics/google-analytics.html +1 -0
- data/_includes/analytics/google-tag-manager-body.html +1 -0
- data/_includes/analytics/google-tag-manager-head.html +1 -0
- data/_includes/analytics/posthog.html +1 -0
- data/_includes/components/README.md +3 -1
- data/_includes/components/admin-links.html +63 -0
- data/_includes/components/admin-tabs.html +1 -0
- data/_includes/components/ai-chat.html +27 -5
- data/_includes/components/author-eeat.html +1 -0
- data/_includes/components/background-customizer.html +1 -0
- data/_includes/components/background-settings.html +36 -37
- data/_includes/components/component-showcase.html +1 -0
- data/_includes/components/cookie-consent.html +1 -0
- data/_includes/components/dev-shortcuts.html +1 -8
- data/_includes/components/env-switcher.html +90 -138
- data/_includes/components/feature-card.html +1 -1
- data/_includes/components/halfmoon.html +22 -41
- data/_includes/components/info-section.html +127 -130
- data/_includes/components/mermaid.html +1 -0
- data/_includes/components/nanobar.html +1 -0
- data/_includes/components/page-feedback.html +252 -0
- data/_includes/components/post-type-badge.html +1 -0
- data/_includes/components/preview-image.html +1 -0
- data/_includes/components/search-modal.html +1 -0
- data/_includes/components/searchbar.html +1 -0
- data/_includes/components/shortcuts-modal.html +1 -0
- data/_includes/components/theme-info.html +21 -30
- data/_includes/components/theme-preview-gallery.html +1 -0
- data/_includes/content/backlinks.html +1 -0
- data/_includes/content/giscus.html +1 -0
- data/_includes/content/intro.html +20 -62
- data/_includes/content/jsonld-faq.html +1 -0
- data/_includes/content/seo.html +1 -0
- data/_includes/content/sitemap.html +1 -0
- data/_includes/content/toc.html +1 -0
- data/_includes/core/color-mode-init.html +1 -0
- data/_includes/core/console-capture.html +89 -0
- data/_includes/core/footer.html +1 -0
- data/_includes/core/head.html +5 -0
- data/_includes/core/header.html +5 -15
- data/_includes/navigation/admin-nav.html +1 -0
- data/_includes/navigation/breadcrumbs.html +1 -0
- data/_includes/navigation/local-graph-fab.html +3 -9
- data/_includes/navigation/local-graph.html +4 -9
- data/_includes/navigation/navbar.html +1 -0
- data/_includes/navigation/sidebar-categories.html +56 -14
- data/_includes/navigation/sidebar-config.html +115 -0
- data/_includes/navigation/sidebar-folders.html +168 -62
- data/_includes/navigation/sidebar-left.html +29 -52
- data/_includes/navigation/sidebar-nav.html +30 -0
- data/_includes/navigation/sidebar-right.html +1 -0
- data/_includes/navigation/toc-fab.html +3 -9
- data/_includes/navigation/unified-drawer.html +7 -14
- data/_includes/obsidian/full-graph.html +1 -0
- data/_includes/setup/wizard.html +1 -0
- data/_includes/stats/stats-categories.html +1 -0
- data/_includes/stats/stats-header.html +1 -0
- data/_includes/stats/stats-metrics.html +1 -0
- data/_includes/stats/stats-overview.html +1 -0
- data/_includes/stats/stats-tags.html +1 -0
- data/_layouts/admin.html +1 -0
- data/_layouts/article.html +1 -0
- data/_layouts/author.html +1 -0
- data/_layouts/authors.html +1 -0
- data/_layouts/collection.html +1 -0
- data/_layouts/default.html +14 -37
- data/_layouts/home.html +1 -0
- data/_layouts/index.html +1 -0
- data/_layouts/landing.html +1 -0
- data/_layouts/news.html +1 -0
- data/_layouts/note.html +1 -0
- data/_layouts/notebook.html +1 -0
- data/_layouts/root.html +6 -2
- data/_layouts/search.html +1 -0
- data/_layouts/section.html +1 -0
- data/_layouts/setup.html +1 -0
- data/_layouts/sitemap-collection.html +1 -0
- data/_layouts/stats.html +1 -0
- data/_layouts/tag.html +1 -0
- data/_layouts/welcome.html +1 -0
- data/_plugins/author_pages_generator.rb +1 -0
- data/_plugins/obsidian_links.rb +1 -0
- data/_plugins/preview_image_generator.rb +1 -0
- data/_plugins/search_and_sitemap_generator.rb +1 -0
- data/_plugins/theme_version.rb +1 -0
- data/_sass/components/_author.scss +12 -1
- data/_sass/components/_footer.scss +22 -0
- data/_sass/components/_page-feedback.scss +276 -0
- data/_sass/components/_ui-enhancements.scss +9 -0
- data/_sass/core/_docs-layout.scss +3 -78
- data/_sass/core/_navbar.scss +10 -27
- data/_sass/core/_obsidian.scss +2 -2
- data/_sass/core/code-copy.scss +10 -0
- data/_sass/custom.scss +1 -0
- data/_sass/notebooks.scss +1 -0
- data/_sass/theme/_backgrounds.scss +7 -2
- data/_sass/tokens/_layers.scss +4 -0
- data/_sass/utilities/_layout.scss +11 -0
- data/assets/css/main.scss +3 -1
- data/assets/js/ai-chat.js +1 -0
- data/assets/js/author-profile.js +1 -0
- data/assets/js/auto-hide-nav.js +1 -0
- data/assets/js/back-to-top.js +1 -0
- data/assets/js/background-customizer.js +1 -0
- data/assets/js/code-copy.js +1 -0
- data/assets/js/halfmoon.js +23 -22
- data/assets/js/modules/navigation/gestures.js +1 -0
- data/assets/js/modules/navigation/keyboard.js +1 -0
- data/assets/js/modules/navigation/scroll-spy.js +1 -0
- data/assets/js/modules/navigation/sidebar-state.js +1 -0
- data/assets/js/modules/navigation/sidebar-visibility.js +1 -0
- data/assets/js/modules/navigation/smooth-scroll.js +1 -0
- data/assets/js/modules/theme/appearance.js +52 -16
- data/assets/js/nanobar-init.js +1 -0
- data/assets/js/navigation.js +1 -0
- data/assets/js/obsidian-graph.js +1 -0
- data/assets/js/obsidian-wiki-links.js +1 -0
- data/assets/js/page-feedback.js +660 -0
- data/assets/js/palette-generator.js +1 -0
- data/assets/js/particles-source.js +1 -0
- data/assets/js/search-modal.js +1 -0
- data/assets/js/setup-wizard.js +1 -0
- data/assets/js/share-actions.js +1 -0
- data/assets/js/skin-editor.js +1 -0
- data/assets/js/table-copy.js +1 -0
- data/assets/js/theme-customizer.js +1 -0
- data/scripts/analyze-commits.sh +1 -0
- data/scripts/bin/giscus-discussions +1 -0
- data/scripts/bin/install +1 -0
- data/scripts/bin/validate +1 -0
- data/scripts/build +1 -0
- data/scripts/content-review.rb +1 -0
- data/scripts/convert-notebooks.sh +4 -3
- data/scripts/fix-markdown-format.sh +1 -0
- data/scripts/generate-preview-images.sh +1 -0
- data/scripts/generate-roadmap.sh +1 -0
- data/scripts/install-preview-generator.sh +1 -0
- data/scripts/release +1 -0
- data/scripts/requirements.txt +11 -0
- data/scripts/setup.sh +1 -0
- data/scripts/tag-features +136 -0
- data/scripts/validate-features.rb +7 -0
- data/scripts/vendor-install.sh +1 -0
- metadata +13 -4
- data/_data/prompts.yml +0 -302
- data/_plugins/admin_page_urls.rb +0 -16
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ===================================================================
|
|
3
|
+
* page-feedback.js — "Improve this page" capture widget
|
|
4
|
+
* ===================================================================
|
|
5
|
+
*
|
|
6
|
+
* File: assets/js/page-feedback.js
|
|
7
|
+
* Path: assets/js/page-feedback.js
|
|
8
|
+
* Purpose: Assemble a well-formed GitHub issue at RUNTIME from a request
|
|
9
|
+
* type + a description + live page context + captured console
|
|
10
|
+
* logs, then either open a pre-filled github.com form (base layer,
|
|
11
|
+
* no token) or file it through the chat proxy (proxy mode), with
|
|
12
|
+
* an optional AI clarify/vet/prioritize step in between.
|
|
13
|
+
*
|
|
14
|
+
* Reads three JSON blocks injected by _includes/components/page-feedback.html:
|
|
15
|
+
* #pageFeedbackConfig — mode, endpoints, repo, labels, assignee, ai flags
|
|
16
|
+
* #pageFeedbackTypes — the request-type taxonomy (_data/feedback_types.yml)
|
|
17
|
+
* #pageFeedbackContext — build-time page/front-matter context
|
|
18
|
+
*
|
|
19
|
+
* Console/error logs come from _includes/core/console-capture.html
|
|
20
|
+
* (window.__zer0Feedback), captured since page load.
|
|
21
|
+
*
|
|
22
|
+
* Robustness notes:
|
|
23
|
+
* - Every value is injected as JSON (not URL-encoded into an href), so the
|
|
24
|
+
* old three-encoder escaping bugs are gone.
|
|
25
|
+
* - GitHub silently drops labels that don't exist, so the taxonomy maps to
|
|
26
|
+
* labels that do (see _data/feedback_types.yml).
|
|
27
|
+
* - Pre-filled issue URLs have a practical length ceiling; buildPrefillUrl()
|
|
28
|
+
* trims low-priority sections and falls back to the clipboard so nothing
|
|
29
|
+
* is silently lost.
|
|
30
|
+
* ===================================================================
|
|
31
|
+
*/
|
|
32
|
+
(function () {
|
|
33
|
+
"use strict";
|
|
34
|
+
|
|
35
|
+
// Pre-filled github.com/issues/new URLs get unreliable past ~8k chars
|
|
36
|
+
// (browser + GitHub limits). Stay comfortably under it and trim to fit.
|
|
37
|
+
var MAX_URL_LENGTH = 7000;
|
|
38
|
+
|
|
39
|
+
function readJSON(id, fallback) {
|
|
40
|
+
var el = document.getElementById(id);
|
|
41
|
+
if (!el) return fallback;
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(el.textContent);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var CONFIG = readJSON("pageFeedbackConfig", null);
|
|
50
|
+
if (!CONFIG || !CONFIG.enabled) return;
|
|
51
|
+
var TYPES = readJSON("pageFeedbackTypes", []) || [];
|
|
52
|
+
var CONTEXT = readJSON("pageFeedbackContext", {}) || {};
|
|
53
|
+
|
|
54
|
+
var modal = document.getElementById("pageFeedbackModal");
|
|
55
|
+
if (!modal) return;
|
|
56
|
+
|
|
57
|
+
// --- element refs -----------------------------------------------------
|
|
58
|
+
var els = {
|
|
59
|
+
dialog: modal.querySelector(".pf-dialog"),
|
|
60
|
+
form: document.getElementById("pfForm"),
|
|
61
|
+
close: document.getElementById("pfClose"),
|
|
62
|
+
cancel: document.getElementById("pfCancel"),
|
|
63
|
+
submit: document.getElementById("pfSubmit"),
|
|
64
|
+
submitLabel: modal.querySelector(".pf-submit-label"),
|
|
65
|
+
description: document.getElementById("pfDescription"),
|
|
66
|
+
types: Array.prototype.slice.call(modal.querySelectorAll(".pf-type")),
|
|
67
|
+
contextList: document.getElementById("pfContextList"),
|
|
68
|
+
contextCount: document.getElementById("pfContextCount"),
|
|
69
|
+
logsWrap: document.getElementById("pfLogsWrap"),
|
|
70
|
+
includeLogs: document.getElementById("pfIncludeLogs"),
|
|
71
|
+
logsCount: document.getElementById("pfLogsCount"),
|
|
72
|
+
logsPreview: document.getElementById("pfLogsPreview"),
|
|
73
|
+
copy: document.getElementById("pfCopy"),
|
|
74
|
+
status: document.getElementById("pfStatus"),
|
|
75
|
+
analyze: document.getElementById("pfAnalyze"),
|
|
76
|
+
aiResult: document.getElementById("pfAiResult")
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
var state = {
|
|
80
|
+
open: false,
|
|
81
|
+
typeId: null,
|
|
82
|
+
analysis: null, // AI enrichment result, when applied
|
|
83
|
+
logs: [], // snapshot taken when the modal opens
|
|
84
|
+
lastFocus: null
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function typeById(id) {
|
|
88
|
+
for (var i = 0; i < TYPES.length; i++) {
|
|
89
|
+
if (TYPES[i].id === id) return TYPES[i];
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- runtime context (captured fresh each open) -----------------------
|
|
95
|
+
function runtimeContext() {
|
|
96
|
+
var mm = window.matchMedia;
|
|
97
|
+
return {
|
|
98
|
+
href: location.href,
|
|
99
|
+
referrer: document.referrer || "",
|
|
100
|
+
userAgent: navigator.userAgent || "",
|
|
101
|
+
language: navigator.language || "",
|
|
102
|
+
viewport: window.innerWidth + "×" + window.innerHeight,
|
|
103
|
+
dpr: window.devicePixelRatio || 1,
|
|
104
|
+
colorScheme: mm && mm("(prefers-color-scheme: dark)").matches ? "dark" : "light",
|
|
105
|
+
reducedMotion: !!(mm && mm("(prefers-reduced-motion: reduce)").matches),
|
|
106
|
+
time: new Date().toISOString()
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function snapshotLogs() {
|
|
111
|
+
var store = window.__zer0Feedback;
|
|
112
|
+
if (!store || typeof store.snapshot !== "function") return [];
|
|
113
|
+
try { return store.snapshot(); } catch (err) { return []; }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatLogs(logs) {
|
|
117
|
+
return logs.map(function (entry) {
|
|
118
|
+
var level = String(entry.level || "log").toUpperCase();
|
|
119
|
+
return "[" + entry.t + "ms] " + level + " " + entry.text;
|
|
120
|
+
}).join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- rendering --------------------------------------------------------
|
|
124
|
+
function renderContext() {
|
|
125
|
+
var rt = runtimeContext();
|
|
126
|
+
var rows = [
|
|
127
|
+
["Page", CONTEXT.title || document.title],
|
|
128
|
+
["URL", rt.href]
|
|
129
|
+
];
|
|
130
|
+
if (CONTEXT.sourceUrl) {
|
|
131
|
+
rows.push(["Source", CONTEXT.path || CONTEXT.sourceUrl]);
|
|
132
|
+
}
|
|
133
|
+
els.contextList.innerHTML = "";
|
|
134
|
+
rows.forEach(function (row) {
|
|
135
|
+
var li = document.createElement("li");
|
|
136
|
+
var k = document.createElement("span");
|
|
137
|
+
k.className = "pf-context-key";
|
|
138
|
+
k.textContent = row[0];
|
|
139
|
+
var v = document.createElement("span");
|
|
140
|
+
v.className = "pf-context-val";
|
|
141
|
+
v.textContent = row[1];
|
|
142
|
+
li.appendChild(k);
|
|
143
|
+
li.appendChild(v);
|
|
144
|
+
els.contextList.appendChild(li);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Logs
|
|
148
|
+
state.logs = CONFIG.captureLogs ? snapshotLogs() : [];
|
|
149
|
+
var count = state.logs.length;
|
|
150
|
+
if (els.logsWrap) {
|
|
151
|
+
if (count > 0) {
|
|
152
|
+
els.logsWrap.hidden = false;
|
|
153
|
+
if (els.logsCount) els.logsCount.textContent = String(count);
|
|
154
|
+
if (els.logsPreview) els.logsPreview.textContent = formatLogs(state.logs);
|
|
155
|
+
} else {
|
|
156
|
+
els.logsWrap.hidden = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
var attached = rows.length + (count > 0 && els.includeLogs && els.includeLogs.checked ? 1 : 0);
|
|
161
|
+
if (els.contextCount) els.contextCount.textContent = "(" + attached + ")";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function setType(id) {
|
|
165
|
+
state.typeId = id;
|
|
166
|
+
var type = typeById(id);
|
|
167
|
+
els.types.forEach(function (btn) {
|
|
168
|
+
var active = btn.getAttribute("data-pf-type-id") === id;
|
|
169
|
+
btn.setAttribute("aria-checked", active ? "true" : "false");
|
|
170
|
+
btn.tabIndex = active ? 0 : -1;
|
|
171
|
+
btn.classList.toggle("pf-type--active", active);
|
|
172
|
+
});
|
|
173
|
+
if (type && type.placeholder && els.description) {
|
|
174
|
+
els.description.placeholder = type.placeholder;
|
|
175
|
+
}
|
|
176
|
+
if (els.submitLabel) {
|
|
177
|
+
var agently = type && type.agent && CONFIG.assignee;
|
|
178
|
+
els.submitLabel.textContent = submitVerb();
|
|
179
|
+
if (agently) {
|
|
180
|
+
els.submit.title = "Files the issue and assigns @" + CONFIG.assignee;
|
|
181
|
+
} else {
|
|
182
|
+
els.submit.removeAttribute("title");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
updateValidity();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function submitVerb() {
|
|
189
|
+
return CONFIG.mode === "proxy" ? "File issue" : "Open GitHub issue";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function updateValidity() {
|
|
193
|
+
var ok = !!state.typeId && els.description && els.description.value.trim().length > 0;
|
|
194
|
+
els.submit.disabled = !ok;
|
|
195
|
+
return ok;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- issue assembly ---------------------------------------------------
|
|
199
|
+
// Escape a value for a Markdown table cell. Backslashes MUST be escaped first,
|
|
200
|
+
// otherwise escaping "|" -> "\|" would be re-processed and corrupted.
|
|
201
|
+
function cell(value) {
|
|
202
|
+
return String(value == null ? "" : value)
|
|
203
|
+
.replace(/\\/g, "\\\\") // backslash first
|
|
204
|
+
.replace(/\|/g, "\\|") // then the cell separator
|
|
205
|
+
.replace(/\r?\n/g, " "); // collapse newlines so the row stays intact
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function dedupe(list) {
|
|
209
|
+
var seen = {};
|
|
210
|
+
var out = [];
|
|
211
|
+
(list || []).forEach(function (item) {
|
|
212
|
+
if (item && !seen[item]) { seen[item] = true; out.push(item); }
|
|
213
|
+
});
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function issueLabels(type) {
|
|
218
|
+
var labels = (CONFIG.defaultLabels || []).concat(type && type.labels ? type.labels : []);
|
|
219
|
+
if (state.analysis && state.analysis.labels) labels = labels.concat(state.analysis.labels);
|
|
220
|
+
if (state.analysis && state.analysis.priority) labels.push("priority:" + state.analysis.priority);
|
|
221
|
+
return dedupe(labels);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Build the issue body in named sections so the URL guard can drop the
|
|
225
|
+
// low-priority ones (logs, then agent directive) without losing the core.
|
|
226
|
+
function buildSections(type, opts) {
|
|
227
|
+
var rt = runtimeContext();
|
|
228
|
+
var includeLogs = opts.includeLogs && state.logs.length > 0;
|
|
229
|
+
var sections = {};
|
|
230
|
+
|
|
231
|
+
sections.description = els.description.value.trim();
|
|
232
|
+
|
|
233
|
+
if (state.analysis) {
|
|
234
|
+
var a = state.analysis;
|
|
235
|
+
var ai = ["## 🤖 AI review", ""];
|
|
236
|
+
if (a.summary) ai.push(a.summary, "");
|
|
237
|
+
var meta = [];
|
|
238
|
+
if (a.priority) meta.push("**Priority:** " + a.priority);
|
|
239
|
+
if (a.severity) meta.push("**Severity:** " + a.severity);
|
|
240
|
+
if (meta.length) ai.push(meta.join(" · "), "");
|
|
241
|
+
if (a.recommendation) ai.push("**Recommended approach:** " + a.recommendation, "");
|
|
242
|
+
sections.ai = ai.join("\n");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
sections.context = [
|
|
246
|
+
"## 📄 Page context", "",
|
|
247
|
+
"| Field | Value |", "|---|---|",
|
|
248
|
+
"| **Page** | " + cell(CONTEXT.title) + " |",
|
|
249
|
+
"| **URL** | " + cell(rt.href) + " |",
|
|
250
|
+
CONTEXT.sourceUrl ? "| **Source** | [`" + cell(CONTEXT.path) + "`](" + CONTEXT.sourceUrl + ") |" : "",
|
|
251
|
+
CONTEXT.layout ? "| **Layout** | `" + cell(CONTEXT.layout) + "` |" : "",
|
|
252
|
+
CONTEXT.collection ? "| **Collection** | `" + cell(CONTEXT.collection) + "` |" : ""
|
|
253
|
+
].filter(Boolean).join("\n");
|
|
254
|
+
|
|
255
|
+
sections.environment = [
|
|
256
|
+
"## 🔧 Environment", "",
|
|
257
|
+
"| Field | Value |", "|---|---|",
|
|
258
|
+
"| **Browser** | " + cell(rt.userAgent) + " |",
|
|
259
|
+
"| **Viewport** | " + cell(rt.viewport) + " @ " + rt.dpr + "x |",
|
|
260
|
+
"| **Color scheme** | " + cell(rt.colorScheme) + (rt.reducedMotion ? " · reduced-motion" : "") + " |",
|
|
261
|
+
rt.referrer ? "| **Referrer** | " + cell(rt.referrer) + " |" : "",
|
|
262
|
+
"| **Repository** | `" + cell(CONTEXT.repository) + "` |",
|
|
263
|
+
"| **Branch** | `" + cell(CONTEXT.branch) + "` |",
|
|
264
|
+
"| **Jekyll env** | `" + cell(CONTEXT.jekyllEnv) + "` |",
|
|
265
|
+
"| **Captured** | " + cell(rt.time) + " |"
|
|
266
|
+
].filter(Boolean).join("\n");
|
|
267
|
+
|
|
268
|
+
if (includeLogs) {
|
|
269
|
+
sections.logs = [
|
|
270
|
+
"<details>",
|
|
271
|
+
"<summary>🧾 Console & error logs (" + state.logs.length + ")</summary>",
|
|
272
|
+
"", "```text", formatLogs(state.logs), "```", "",
|
|
273
|
+
"</details>"
|
|
274
|
+
].join("\n");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (type && type.agent && type.directive) {
|
|
278
|
+
sections.directive = [
|
|
279
|
+
"<details>",
|
|
280
|
+
"<summary>🤖 AI agent instructions</summary>",
|
|
281
|
+
"", type.directive, "",
|
|
282
|
+
"</details>"
|
|
283
|
+
].join("\n");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
sections.footer = "---\n_Filed from " + rt.href + " via the page-feedback widget._";
|
|
287
|
+
return sections;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function assemble(sections, order) {
|
|
291
|
+
return order
|
|
292
|
+
.map(function (key) { return sections[key]; })
|
|
293
|
+
.filter(function (part) { return part && part.length; })
|
|
294
|
+
.join("\n\n");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildIssue(opts) {
|
|
298
|
+
opts = opts || {};
|
|
299
|
+
var type = typeById(state.typeId);
|
|
300
|
+
var includeLogs = els.includeLogs ? els.includeLogs.checked : false;
|
|
301
|
+
var sections = buildSections(type, { includeLogs: opts.includeLogs !== false && includeLogs });
|
|
302
|
+
|
|
303
|
+
var titleBase = state.analysis && state.analysis.title
|
|
304
|
+
? state.analysis.title
|
|
305
|
+
: (type ? "[" + type.label + "] " : "") + (CONTEXT.title || document.title);
|
|
306
|
+
|
|
307
|
+
var fullOrder = ["description", "ai", "context", "environment", "logs", "directive", "footer"];
|
|
308
|
+
var assignees = type && type.agent && CONFIG.assignee ? [CONFIG.assignee] : [];
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
title: titleBase.slice(0, 240),
|
|
312
|
+
body: assemble(sections, fullOrder),
|
|
313
|
+
labels: issueLabels(type),
|
|
314
|
+
assignees: assignees,
|
|
315
|
+
sections: sections,
|
|
316
|
+
order: fullOrder
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Assemble a prefill URL under the length budget, trimming logs then the
|
|
321
|
+
// agent directive then the environment table. Reports what was dropped so
|
|
322
|
+
// the caller can offer the full body via the clipboard.
|
|
323
|
+
function buildPrefillUrl(issue) {
|
|
324
|
+
var base = "https://github.com/" + CONFIG.repository + "/issues/new";
|
|
325
|
+
|
|
326
|
+
function compose(order) {
|
|
327
|
+
var params = new URLSearchParams();
|
|
328
|
+
params.set("title", issue.title);
|
|
329
|
+
params.set("body", assemble(issue.sections, order));
|
|
330
|
+
if (issue.labels.length) params.set("labels", issue.labels.join(","));
|
|
331
|
+
if (issue.assignees.length) params.set("assignees", issue.assignees.join(","));
|
|
332
|
+
return base + "?" + params.toString();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
var trimSteps = [
|
|
336
|
+
{ order: ["description", "ai", "context", "environment", "logs", "directive", "footer"], dropped: [] },
|
|
337
|
+
{ order: ["description", "ai", "context", "environment", "directive", "footer"], dropped: ["logs"] },
|
|
338
|
+
{ order: ["description", "ai", "context", "environment", "footer"], dropped: ["logs", "directive"] },
|
|
339
|
+
{ order: ["description", "ai", "context", "footer"], dropped: ["logs", "directive", "environment"] }
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
for (var i = 0; i < trimSteps.length; i++) {
|
|
343
|
+
var url = compose(trimSteps[i].order);
|
|
344
|
+
if (url.length <= MAX_URL_LENGTH || i === trimSteps.length - 1) {
|
|
345
|
+
return { url: url, dropped: trimSteps[i].dropped, overLimit: url.length > MAX_URL_LENGTH };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// --- clipboard --------------------------------------------------------
|
|
351
|
+
function copyText(text) {
|
|
352
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
353
|
+
return navigator.clipboard.writeText(text);
|
|
354
|
+
}
|
|
355
|
+
return new Promise(function (resolve, reject) {
|
|
356
|
+
try {
|
|
357
|
+
var ta = document.createElement("textarea");
|
|
358
|
+
ta.value = text;
|
|
359
|
+
ta.setAttribute("readonly", "");
|
|
360
|
+
ta.style.position = "absolute";
|
|
361
|
+
ta.style.left = "-9999px";
|
|
362
|
+
document.body.appendChild(ta);
|
|
363
|
+
ta.select();
|
|
364
|
+
document.execCommand("copy");
|
|
365
|
+
document.body.removeChild(ta);
|
|
366
|
+
resolve();
|
|
367
|
+
} catch (err) { reject(err); }
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function setStatus(message, kind) {
|
|
372
|
+
if (!els.status) return;
|
|
373
|
+
els.status.textContent = message || "";
|
|
374
|
+
els.status.className = "pf-status" + (kind ? " pf-status--" + kind : "");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// --- submit -----------------------------------------------------------
|
|
378
|
+
function submitViaUrl(issue) {
|
|
379
|
+
var built = buildPrefillUrl(issue);
|
|
380
|
+
// Open WITHOUT the "noopener" feature so we get a real window reference to
|
|
381
|
+
// detect pop-up blocking, then sever the opener link ourselves.
|
|
382
|
+
var win = window.open(built.url, "_blank");
|
|
383
|
+
if (win) { try { win.opener = null; } catch (err) { /* cross-origin */ } }
|
|
384
|
+
|
|
385
|
+
if (!win) {
|
|
386
|
+
copyText(issue.body).then(function () {
|
|
387
|
+
setStatus("Your browser blocked the pop-up. The full report is on your clipboard — open a new issue and paste it in.", "warn");
|
|
388
|
+
})["catch"](function () {
|
|
389
|
+
setStatus("Your browser blocked the pop-up. Allow pop-ups for this site and try again.", "warn");
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (built.dropped.length) {
|
|
394
|
+
// Something was too long for the URL — hand the full body to the clipboard.
|
|
395
|
+
copyText(issue.body).then(function () {
|
|
396
|
+
setStatus("Opened GitHub. The " + built.dropped.join(" and ") +
|
|
397
|
+
" section(s) were too long for the URL, so the full report is on your clipboard — paste it in.", "warn");
|
|
398
|
+
})["catch"](function () {
|
|
399
|
+
setStatus("Opened GitHub. Some sections were trimmed to fit the URL.", "warn");
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
setStatus("Opened a pre-filled GitHub issue in a new tab. ✓", "ok");
|
|
404
|
+
window.setTimeout(close, 1200);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function submitViaProxy(issue) {
|
|
408
|
+
setStatus("Filing the issue…", "pending");
|
|
409
|
+
els.submit.disabled = true;
|
|
410
|
+
fetch(CONFIG.issueEndpoint, {
|
|
411
|
+
method: "POST",
|
|
412
|
+
headers: { "content-type": "application/json" },
|
|
413
|
+
body: JSON.stringify({ title: issue.title, body: issue.body, labels: issue.labels })
|
|
414
|
+
}).then(function (res) {
|
|
415
|
+
return res.json().then(function (data) { return { ok: res.ok, data: data }; });
|
|
416
|
+
}).then(function (r) {
|
|
417
|
+
if (!r.ok) throw new Error((r.data && r.data.error && r.data.error.message) || "Proxy returned an error");
|
|
418
|
+
var url = r.data && r.data.url;
|
|
419
|
+
els.status.innerHTML = "";
|
|
420
|
+
setStatus("", "ok");
|
|
421
|
+
if (url) {
|
|
422
|
+
els.status.innerHTML = 'Filed <a href="' + url + '" target="_blank" rel="noopener">issue #' +
|
|
423
|
+
(r.data.number || "") + "</a> ✓";
|
|
424
|
+
} else {
|
|
425
|
+
setStatus("Issue filed. ✓", "ok");
|
|
426
|
+
}
|
|
427
|
+
})["catch"](function (err) {
|
|
428
|
+
// Fall back to the always-works prefill path.
|
|
429
|
+
setStatus("Couldn't file directly (" + err.message + ") — opening a pre-filled form instead…", "warn");
|
|
430
|
+
submitViaUrl(issue);
|
|
431
|
+
}).then(function () {
|
|
432
|
+
els.submit.disabled = false;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function onSubmit(event) {
|
|
437
|
+
if (event) event.preventDefault();
|
|
438
|
+
if (!updateValidity()) {
|
|
439
|
+
setStatus("Pick a feedback type and add a short description first.", "warn");
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
var issue = buildIssue();
|
|
443
|
+
if (CONFIG.mode === "proxy" && CONFIG.issueEndpoint) {
|
|
444
|
+
submitViaProxy(issue);
|
|
445
|
+
} else {
|
|
446
|
+
submitViaUrl(issue);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// --- AI enrichment ----------------------------------------------------
|
|
451
|
+
function analyze() {
|
|
452
|
+
if (!CONFIG.ai || !CONFIG.ai.available) return;
|
|
453
|
+
if (!state.typeId || !els.description.value.trim()) {
|
|
454
|
+
setStatus("Pick a type and describe the issue before analyzing.", "warn");
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
var type = typeById(state.typeId);
|
|
458
|
+
var includeLogs = els.includeLogs ? els.includeLogs.checked : false;
|
|
459
|
+
var payload = {
|
|
460
|
+
type: { id: type.id, label: type.label, scope: type.scope, agent: !!type.agent, labels: type.labels || [] },
|
|
461
|
+
description: els.description.value.trim(),
|
|
462
|
+
page: {
|
|
463
|
+
title: CONTEXT.title, url: location.href, path: CONTEXT.path,
|
|
464
|
+
sourceUrl: CONTEXT.sourceUrl, layout: CONTEXT.layout,
|
|
465
|
+
collection: CONTEXT.collection, repository: CONTEXT.repository, branch: CONTEXT.branch
|
|
466
|
+
},
|
|
467
|
+
environment: runtimeContext(),
|
|
468
|
+
logs: includeLogs ? formatLogs(state.logs) : "",
|
|
469
|
+
availableLabels: dedupe((CONFIG.defaultLabels || []).concat(collectAllLabels())),
|
|
470
|
+
model: CONFIG.ai.model,
|
|
471
|
+
maxTokens: CONFIG.ai.maxTokens
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
els.analyze.disabled = true;
|
|
475
|
+
els.aiResult.hidden = false;
|
|
476
|
+
els.aiResult.innerHTML = '<span class="pf-ai-spinner" aria-hidden="true"></span> Analyzing with Claude…';
|
|
477
|
+
setStatus("", "");
|
|
478
|
+
|
|
479
|
+
fetch(CONFIG.endpoint, {
|
|
480
|
+
method: "POST",
|
|
481
|
+
headers: { "content-type": "application/json" },
|
|
482
|
+
body: JSON.stringify(payload)
|
|
483
|
+
}).then(function (res) {
|
|
484
|
+
return res.json().then(function (data) { return { ok: res.ok, data: data }; });
|
|
485
|
+
}).then(function (r) {
|
|
486
|
+
if (!r.ok) throw new Error((r.data && r.data.error && r.data.error.message) || "Analysis failed");
|
|
487
|
+
renderAnalysis(r.data);
|
|
488
|
+
})["catch"](function (err) {
|
|
489
|
+
els.aiResult.innerHTML = '<span class="pf-ai-error">AI analysis unavailable (' +
|
|
490
|
+
escapeHtml(err.message) + "). You can still file the issue below.</span>";
|
|
491
|
+
}).then(function () {
|
|
492
|
+
els.analyze.disabled = false;
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function collectAllLabels() {
|
|
497
|
+
var all = [];
|
|
498
|
+
TYPES.forEach(function (t) { if (t.labels) all = all.concat(t.labels); });
|
|
499
|
+
["priority:P0", "priority:P1", "priority:P2", "priority:P3"].forEach(function (p) { all.push(p); });
|
|
500
|
+
return all;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function renderAnalysis(data) {
|
|
504
|
+
state.analysis = {
|
|
505
|
+
title: data.title || null,
|
|
506
|
+
summary: data.summary || "",
|
|
507
|
+
severity: data.severity || "",
|
|
508
|
+
priority: data.priority || "",
|
|
509
|
+
labels: Array.isArray(data.labels) ? data.labels : [],
|
|
510
|
+
recommendation: data.recommendation || "",
|
|
511
|
+
questions: Array.isArray(data.questions) ? data.questions : []
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
var html = ['<div class="pf-ai-card">'];
|
|
515
|
+
if (state.analysis.priority || state.analysis.severity) {
|
|
516
|
+
html.push('<p class="pf-ai-badges">');
|
|
517
|
+
if (state.analysis.priority) html.push('<span class="badge bg-danger-subtle text-danger-emphasis">' + escapeHtml(state.analysis.priority) + "</span> ");
|
|
518
|
+
if (state.analysis.severity) html.push('<span class="badge bg-secondary-subtle text-secondary-emphasis">' + escapeHtml(state.analysis.severity) + "</span>");
|
|
519
|
+
html.push("</p>");
|
|
520
|
+
}
|
|
521
|
+
if (state.analysis.summary) html.push("<p>" + escapeHtml(state.analysis.summary) + "</p>");
|
|
522
|
+
if (state.analysis.recommendation) {
|
|
523
|
+
html.push('<p class="pf-ai-rec"><strong>Suggested approach:</strong> ' + escapeHtml(state.analysis.recommendation) + "</p>");
|
|
524
|
+
}
|
|
525
|
+
if (state.analysis.labels.length) {
|
|
526
|
+
html.push('<p class="pf-ai-labels">Labels: ');
|
|
527
|
+
state.analysis.labels.forEach(function (l) { html.push('<code>' + escapeHtml(l) + "</code> "); });
|
|
528
|
+
html.push("</p>");
|
|
529
|
+
}
|
|
530
|
+
if (state.analysis.questions.length) {
|
|
531
|
+
html.push('<p class="pf-ai-q-title">A couple of clarifying questions — answer them in your description:</p><ul class="pf-ai-questions">');
|
|
532
|
+
state.analysis.questions.forEach(function (q) { html.push("<li>" + escapeHtml(q) + "</li>"); });
|
|
533
|
+
html.push("</ul>");
|
|
534
|
+
}
|
|
535
|
+
html.push('<p class="pf-ai-note text-muted"><i class="bi bi-check2-circle me-1"></i>Applied to your issue — review and file below.</p>');
|
|
536
|
+
html.push("</div>");
|
|
537
|
+
els.aiResult.innerHTML = html.join("");
|
|
538
|
+
setStatus("AI review applied. Review and file when ready.", "ok");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function escapeHtml(text) {
|
|
542
|
+
var div = document.createElement("div");
|
|
543
|
+
div.textContent = String(text == null ? "" : text);
|
|
544
|
+
return div.innerHTML;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// --- open / close / focus trap ---------------------------------------
|
|
548
|
+
function focusable() {
|
|
549
|
+
return Array.prototype.slice.call(
|
|
550
|
+
els.dialog.querySelectorAll('button, [href], input, textarea, select, [tabindex]:not([tabindex="-1"])')
|
|
551
|
+
).filter(function (el) {
|
|
552
|
+
// Exclude disabled, hidden, and roving-tabindex(-1) radios so the trap
|
|
553
|
+
// boundaries land on real, reachable controls.
|
|
554
|
+
return !el.disabled && el.offsetParent !== null && el.tabIndex !== -1;
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function onKeydown(event) {
|
|
559
|
+
if (event.key === "Escape") { event.preventDefault(); close(); return; }
|
|
560
|
+
if (event.key === "Tab") {
|
|
561
|
+
var items = focusable();
|
|
562
|
+
if (!items.length) return;
|
|
563
|
+
var first = items[0];
|
|
564
|
+
var last = items[items.length - 1];
|
|
565
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
566
|
+
event.preventDefault(); last.focus();
|
|
567
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
568
|
+
event.preventDefault(); first.focus();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function open(preselectType) {
|
|
574
|
+
if (state.open) return;
|
|
575
|
+
state.open = true;
|
|
576
|
+
state.lastFocus = document.activeElement;
|
|
577
|
+
modal.hidden = false;
|
|
578
|
+
// reflow before adding the class so the transition runs
|
|
579
|
+
void modal.offsetWidth;
|
|
580
|
+
modal.classList.add("pf-overlay--open");
|
|
581
|
+
document.body.classList.add("pf-no-scroll");
|
|
582
|
+
setStatus("", "");
|
|
583
|
+
if (els.aiResult) { els.aiResult.hidden = true; els.aiResult.innerHTML = ""; }
|
|
584
|
+
state.analysis = null;
|
|
585
|
+
renderContext();
|
|
586
|
+
|
|
587
|
+
var type = preselectType || state.typeId;
|
|
588
|
+
if (type && typeById(type)) {
|
|
589
|
+
setType(type);
|
|
590
|
+
if (els.description) els.description.focus();
|
|
591
|
+
} else if (els.types.length) {
|
|
592
|
+
els.types[0].focus();
|
|
593
|
+
}
|
|
594
|
+
document.addEventListener("keydown", onKeydown, true);
|
|
595
|
+
|
|
596
|
+
if (CONFIG.ai && CONFIG.ai.available && CONFIG.ai.auto && state.typeId) analyze();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function close() {
|
|
600
|
+
if (!state.open) return;
|
|
601
|
+
state.open = false;
|
|
602
|
+
modal.classList.remove("pf-overlay--open");
|
|
603
|
+
document.body.classList.remove("pf-no-scroll");
|
|
604
|
+
document.removeEventListener("keydown", onKeydown, true);
|
|
605
|
+
window.setTimeout(function () { modal.hidden = true; }, 180);
|
|
606
|
+
if (state.lastFocus && state.lastFocus.focus) state.lastFocus.focus();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// --- type keyboard navigation (roving radiogroup) ---------------------
|
|
610
|
+
function onTypeKeydown(event) {
|
|
611
|
+
var idx = els.types.indexOf(event.currentTarget);
|
|
612
|
+
if (idx < 0) return;
|
|
613
|
+
var next = null;
|
|
614
|
+
if (event.key === "ArrowDown" || event.key === "ArrowRight") next = els.types[idx + 1] || els.types[0];
|
|
615
|
+
else if (event.key === "ArrowUp" || event.key === "ArrowLeft") next = els.types[idx - 1] || els.types[els.types.length - 1];
|
|
616
|
+
else if (event.key === " " || event.key === "Enter") { event.preventDefault(); setType(event.currentTarget.getAttribute("data-pf-type-id")); return; }
|
|
617
|
+
if (next) { event.preventDefault(); next.focus(); setType(next.getAttribute("data-pf-type-id")); }
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// --- wire up ----------------------------------------------------------
|
|
621
|
+
els.types.forEach(function (btn) {
|
|
622
|
+
btn.addEventListener("click", function () { setType(btn.getAttribute("data-pf-type-id")); });
|
|
623
|
+
btn.addEventListener("keydown", onTypeKeydown);
|
|
624
|
+
});
|
|
625
|
+
if (els.description) els.description.addEventListener("input", function () { updateValidity(); renderContextCount(); });
|
|
626
|
+
if (els.includeLogs) els.includeLogs.addEventListener("change", renderContextCount);
|
|
627
|
+
if (els.form) els.form.addEventListener("submit", onSubmit);
|
|
628
|
+
if (els.close) els.close.addEventListener("click", close);
|
|
629
|
+
if (els.cancel) els.cancel.addEventListener("click", close);
|
|
630
|
+
if (els.analyze) els.analyze.addEventListener("click", analyze);
|
|
631
|
+
if (els.copy) els.copy.addEventListener("click", function () {
|
|
632
|
+
if (!state.typeId) { setStatus("Pick a feedback type first.", "warn"); return; }
|
|
633
|
+
copyText(buildIssue().body).then(function () { setStatus("Full report copied to your clipboard. ✓", "ok"); })
|
|
634
|
+
["catch"](function () { setStatus("Couldn't access the clipboard.", "warn"); });
|
|
635
|
+
});
|
|
636
|
+
// Close when the backdrop (outside the dialog) is clicked.
|
|
637
|
+
modal.addEventListener("mousedown", function (event) {
|
|
638
|
+
if (event.target === modal) close();
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
function renderContextCount() {
|
|
642
|
+
var base = CONTEXT.sourceUrl ? 3 : 2;
|
|
643
|
+
var withLogs = state.logs.length > 0 && els.includeLogs && els.includeLogs.checked ? 1 : 0;
|
|
644
|
+
if (els.contextCount) els.contextCount.textContent = "(" + (base + withLogs) + ")";
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Any [data-pf-open] element opens the modal; data-pf-type preselects a type.
|
|
648
|
+
document.addEventListener("click", function (event) {
|
|
649
|
+
var trigger = event.target.closest ? event.target.closest("[data-pf-open]") : null;
|
|
650
|
+
if (!trigger) return;
|
|
651
|
+
event.preventDefault();
|
|
652
|
+
open(trigger.getAttribute("data-pf-type") || null);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// Update the submit verb to match the configured mode.
|
|
656
|
+
if (els.submitLabel) els.submitLabel.textContent = submitVerb();
|
|
657
|
+
|
|
658
|
+
// Expose a tiny programmatic hook (used by tests / other scripts).
|
|
659
|
+
window.zer0PageFeedback = { open: open, close: close };
|
|
660
|
+
})();
|
data/assets/js/search-modal.js
CHANGED
data/assets/js/setup-wizard.js
CHANGED
data/assets/js/share-actions.js
CHANGED