jekyll-theme-zer0 1.28.0 → 1.30.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.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1143 -13
  3. data/_data/README.md +2 -0
  4. data/_data/ai.yml +5 -3
  5. data/_data/ai_pricing.yml +36 -0
  6. data/_data/backlog.yml +507 -2
  7. data/_data/consumers.yml +157 -9
  8. data/_data/features.yml +303 -20
  9. data/_data/feedback_types.yml +17 -12
  10. data/_data/i18n/fr.yml +12 -7
  11. data/_data/i18n/manifest.yml +39 -12
  12. data/_data/ingredient_densities.yml +122 -0
  13. data/_data/landing.yml +5 -2
  14. data/_data/navigation/main.yml +16 -0
  15. data/_data/navigation/quickstart.yml +4 -0
  16. data/_data/recipe_courses.yml +64 -0
  17. data/_data/site_builder.yml +874 -0
  18. data/_data/theme-manifest.yml +160 -124
  19. data/_data/ui-text.yml +26 -0
  20. data/_includes/README.md +26 -2
  21. data/_includes/analytics/posthog.html +2 -2
  22. data/_includes/components/admin-links.html +2 -2
  23. data/_includes/components/admin-tabs.html +2 -2
  24. data/_includes/components/ai-chat.html +14 -11
  25. data/_includes/components/analytics-dashboard.html +8 -8
  26. data/_includes/components/author-bio.html +1 -1
  27. data/_includes/components/author-card.html +10 -2
  28. data/_includes/components/author-eeat.html +4 -4
  29. data/_includes/components/background-customizer.html +10 -10
  30. data/_includes/components/background-image.html +114 -0
  31. data/_includes/components/background-settings.html +28 -15
  32. data/_includes/components/collection-manager.html +5 -5
  33. data/_includes/components/component-showcase.html +13 -13
  34. data/_includes/components/config-editor.html +12 -12
  35. data/_includes/components/config-viewer.html +8 -8
  36. data/_includes/components/cookie-consent.html +15 -15
  37. data/_includes/components/cta-button.html +7 -2
  38. data/_includes/components/dev-shortcuts.html +7 -7
  39. data/_includes/components/env-dashboard.html +8 -8
  40. data/_includes/components/env-switcher.html +9 -9
  41. data/_includes/components/feature-card.html +2 -2
  42. data/_includes/components/halfmoon.html +2 -2
  43. data/_includes/components/info-section.html +42 -37
  44. data/_includes/components/js-cdn.html +15 -15
  45. data/_includes/components/language-toggle.html +168 -21
  46. data/_includes/components/mermaid.html +72 -435
  47. data/_includes/components/nanobar.html +5 -5
  48. data/_includes/components/nav-editor.html +2 -2
  49. data/_includes/components/nav-export.html +2 -2
  50. data/_includes/components/nav-overview.html +2 -2
  51. data/_includes/components/page-feedback.html +45 -30
  52. data/_includes/components/page-views-init.html +55 -0
  53. data/_includes/components/page-views.html +33 -0
  54. data/_includes/components/post-card.html +22 -22
  55. data/_includes/components/post-type-badge.html +2 -2
  56. data/_includes/components/powered-by.html +2 -2
  57. data/_includes/components/preview-image.html +6 -0
  58. data/_includes/components/quick-index.html +2 -2
  59. data/_includes/components/recipe-card.html +67 -0
  60. data/_includes/components/recipe-duration.html +50 -0
  61. data/_includes/components/recipe-grams.html +58 -0
  62. data/_includes/components/recipe-index.html +96 -0
  63. data/_includes/components/recipe-ingredients.html +90 -0
  64. data/_includes/components/recipe-meta.html +96 -0
  65. data/_includes/components/recipe-nutrition.html +57 -0
  66. data/_includes/components/recipe-qty.html +73 -0
  67. data/_includes/components/recipe-ratio.html +151 -0
  68. data/_includes/components/recipe-scaler.html +73 -0
  69. data/_includes/components/recipe-steps.html +86 -0
  70. data/_includes/components/recipe-temp.html +45 -0
  71. data/_includes/components/search-modal.html +29 -4
  72. data/_includes/components/searchbar.html +2 -2
  73. data/_includes/components/shortcuts-modal.html +3 -0
  74. data/_includes/components/svg-background.html +2 -2
  75. data/_includes/components/theme-customizer.html +2 -2
  76. data/_includes/components/theme-info.html +14 -7
  77. data/_includes/components/theme-preview-gallery.html +22 -22
  78. data/_includes/content/giscus.html +2 -2
  79. data/_includes/content/intro.html +8 -8
  80. data/_includes/content/jsonld-faq.html +2 -2
  81. data/_includes/content/jsonld-software.html +24 -5
  82. data/_includes/content/seo.html +4 -4
  83. data/_includes/content/sitemap.html +27 -27
  84. data/_includes/content/toc.html +183 -183
  85. data/_includes/core/branding.html +6 -6
  86. data/_includes/core/console-capture.html +32 -74
  87. data/_includes/core/favicon.html +49 -7
  88. data/_includes/core/footer-fabs.html +17 -3
  89. data/_includes/core/footer.html +49 -34
  90. data/_includes/core/head.html +110 -86
  91. data/_includes/core/header.html +76 -54
  92. data/_includes/docs/bootstrap-docs.html +8 -8
  93. data/_includes/landing/landing-install-cards.html +2 -2
  94. data/_includes/landing/landing-quick-links.html +1 -1
  95. data/_includes/navigation/admin-nav.html +2 -2
  96. data/_includes/navigation/nav-tree.html +8 -8
  97. data/_includes/navigation/navbar.html +12 -12
  98. data/_includes/navigation/section-sidebar.html +109 -27
  99. data/_includes/navigation/sidebar-config.html +36 -2
  100. data/_includes/navigation/sidebar-left.html +17 -16
  101. data/_includes/navigation/sidebar-right.html +8 -7
  102. data/_includes/obsidian/full-graph.html +2 -2
  103. data/_includes/setup/claude-session.html +72 -0
  104. data/_includes/setup/prereq-checklist.html +90 -0
  105. data/_includes/setup/wizard.html +924 -222
  106. data/_includes/stats/stats-categories.html +8 -8
  107. data/_includes/stats/stats-header.html +14 -14
  108. data/_includes/stats/stats-metrics.html +14 -14
  109. data/_includes/stats/stats-no-data.html +12 -12
  110. data/_includes/stats/stats-overview.html +6 -6
  111. data/_includes/stats/stats-tags.html +8 -8
  112. data/_layouts/404.html +38 -24
  113. data/_layouts/README.md +2 -0
  114. data/_layouts/admin.html +24 -24
  115. data/_layouts/article.html +43 -33
  116. data/_layouts/author.html +20 -20
  117. data/_layouts/authors.html +2 -2
  118. data/_layouts/book-abc.html +12 -12
  119. data/_layouts/book-story.html +15 -15
  120. data/_layouts/book.html +12 -12
  121. data/_layouts/collection.html +33 -33
  122. data/_layouts/cookbook.html +88 -0
  123. data/_layouts/default.html +31 -28
  124. data/_layouts/home.html +23 -23
  125. data/_layouts/index.html +10 -10
  126. data/_layouts/landing.html +17 -17
  127. data/_layouts/news.html +44 -44
  128. data/_layouts/note.html +38 -38
  129. data/_layouts/notebook.html +34 -34
  130. data/_layouts/recipe.html +274 -0
  131. data/_layouts/root.html +92 -55
  132. data/_layouts/section.html +62 -33
  133. data/_layouts/setup.html +3 -3
  134. data/_layouts/sitemap-collection.html +49 -49
  135. data/_layouts/stats.html +40 -40
  136. data/_layouts/tag.html +12 -12
  137. data/_layouts/welcome.html +21 -21
  138. data/_sass/components/_callout.scss +1 -1
  139. data/_sass/components/_footer.scss +37 -1
  140. data/_sass/components/_mermaid.scss +375 -0
  141. data/_sass/components/_page-views.scss +36 -0
  142. data/_sass/components/_recipe.scss +506 -0
  143. data/_sass/components/_setup-wizard.scss +764 -0
  144. data/_sass/components/_ui-enhancements.scss +6 -6
  145. data/_sass/core/_navbar.scss +261 -46
  146. data/_sass/layouts/_landing.scss +2 -2
  147. data/_sass/layouts/_navbar-extras.scss +14 -4
  148. data/_sass/tokens/_color.scss +6 -0
  149. data/_sass/tokens/_index.scss +2 -0
  150. data/_sass/tokens/_radius.scss +21 -0
  151. data/_sass/tokens/_typography.scss +4 -0
  152. data/_sass/utilities/_focus.scss +14 -0
  153. data/assets/css/main.scss +4 -0
  154. data/assets/js/ai-chat.js +47 -5
  155. data/assets/js/fleet-feedback-capture.js +124 -0
  156. data/assets/js/fleet-feedback.js +853 -0
  157. data/assets/js/mermaid-diagrams.js +1267 -0
  158. data/assets/js/modules/navigation/config.js +9 -6
  159. data/assets/js/modules/navigation/navbar.js +55 -0
  160. data/assets/js/modules/navigation/scroll-spy.js +315 -80
  161. data/assets/js/modules/theme/appearance.js +8 -2
  162. data/assets/js/obsidian-wiki-links.js +8 -3
  163. data/assets/js/page-feedback.js +125 -192
  164. data/assets/js/page-views.js +372 -0
  165. data/assets/js/recipe-scaler.js +501 -0
  166. data/assets/js/search-modal.js +36 -0
  167. data/assets/js/setup-wizard.js +2279 -226
  168. data/assets/js/site-builder.js +1834 -0
  169. data/assets/js/ui-enhancements.js +11 -3
  170. data/scripts/README.md +44 -0
  171. data/scripts/ai/README.md +38 -0
  172. data/scripts/ai/api_call.rb +124 -0
  173. data/scripts/ai/usage.rb +314 -0
  174. data/scripts/ai/usage_report.rb +225 -0
  175. data/scripts/bin/audit-consumer +39 -7
  176. data/scripts/bin/giscus-discussions +213 -14
  177. data/scripts/bin/manifest +35 -12
  178. data/scripts/ci/agent_review_result.py +164 -0
  179. data/scripts/ci/test_agent_review_result.py +172 -0
  180. data/scripts/ci/test_visual_evidence_autogen.py +341 -0
  181. data/scripts/ci/visual_evidence_autogen.py +1060 -0
  182. data/scripts/content-review.rb +20 -1
  183. data/scripts/design-system-check.rb +170 -0
  184. data/scripts/lib/audit.sh +42 -2
  185. data/scripts/lint-liquid-raw.rb +137 -0
  186. data/scripts/test/integration/mermaid +22 -8
  187. data/scripts/test/lib/run_tests.sh +3 -1
  188. data/scripts/test/lib/test_agent_review_result.sh +27 -0
  189. data/scripts/test/lib/test_visual_evidence_autogen.sh +24 -0
  190. data/scripts/translate.rb +94 -16
  191. metadata +48 -2
@@ -5,36 +5,52 @@
5
5
  *
6
6
  * File: assets/js/page-feedback.js
7
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.
8
+ * Purpose: The theme's UI for turning what a reader is looking at into a
9
+ * well-formed GitHub issue — request type, description, live page
10
+ * context, captured console logs — opened as a pre-filled
11
+ * github.com form (base layer, no token) or filed through the chat
12
+ * proxy (proxy mode), with an optional AI clarify/vet/prioritize
13
+ * step in between.
14
+ *
15
+ * THE ISSUE BODY IS NOT BUILT HERE. It comes from FleetFeedbackCore in the
16
+ * vendored assets/js/fleet-feedback.js (bamr87/bamr87 templates/feedback,
17
+ * spec specs/FEEDBACK.md UPS-FB) — the same builder the fleet's
18
+ * <fleet-feedback> web component uses. This file owns the Bootstrap modal,
19
+ * the AI enrichment step, and the submit paths; the core owns the contract:
20
+ * section order, the tables, the `<!-- fleet-feedback v1 type=... -->` marker
21
+ * the issue pipeline reads, labels, assignment, and the 7000-char URL budget
22
+ * with its trim ladder and clipboard fallback.
23
+ *
24
+ * That split is the point. This widget and the fleet component are two UIs
25
+ * over one contract, so an issue filed from a Jekyll page and one filed from
26
+ * a Next.js dashboard are byte-identical to the pipeline that triages them —
27
+ * instead of two shapes that drift apart a release at a time.
13
28
  *
14
29
  * Reads three JSON blocks injected by _includes/components/page-feedback.html:
15
30
  * #pageFeedbackConfig — mode, endpoints, repo, labels, assignee, ai flags
16
31
  * #pageFeedbackTypes — the request-type taxonomy (_data/feedback_types.yml)
17
32
  * #pageFeedbackContext — build-time page/front-matter context
18
33
  *
19
- * Console/error logs come from _includes/core/console-capture.html
20
- * (window.__zer0Feedback), captured since page load.
34
+ * Console/error logs come from the same kit's capture buffer
35
+ * (window.__fleetFeedback), installed in <head> by core/console-capture.html
36
+ * and redacted on the way in.
21
37
  *
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.
38
+ * Note: GitHub silently drops labels that don't exist — no error, no warning
39
+ * — so the taxonomy maps onto the fleet issue-pipeline label set, all of
40
+ * which exist in this repo (see _data/feedback_types.yml).
30
41
  * ===================================================================
31
42
  */
32
43
  (function () {
33
44
  "use strict";
34
45
 
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;
46
+ // The shared issue builder (assets/js/fleet-feedback.js, vendored from the
47
+ // hub kit). Without it there is no contract to emit, so bail rather than
48
+ // silently falling back to a second, divergent body format.
49
+ var FF = window.FleetFeedbackCore;
50
+ if (!FF) {
51
+ console.warn("page-feedback: FleetFeedbackCore is missing — is assets/js/fleet-feedback.js loaded?");
52
+ return;
53
+ }
38
54
 
39
55
  function readJSON(id, fallback) {
40
56
  var el = document.getElementById(id);
@@ -91,41 +107,44 @@
91
107
  return null;
92
108
  }
93
109
 
94
- // --- runtime context (captured fresh each open) -----------------------
95
- function runtimeContext() {
96
- var mm = window.matchMedia;
110
+ // --- page + environment, as the shared builder expects them ----------
111
+ // The core owns which rows the tables carry; the extra `rows` are this
112
+ // stack's own detail (Jekyll build env, theme, layout) appended after the
113
+ // contract rows rather than replacing them.
114
+ function pageContext() {
97
115
  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()
116
+ title: CONTEXT.title || document.title,
117
+ url: location.href,
118
+ source: CONTEXT.path || "",
119
+ sourceUrl: CONTEXT.sourceUrl || "",
120
+ route: CONTEXT.collection || CONTEXT.url || "",
121
+ lastmod: CONTEXT.lastmod || "",
122
+ rows: CONTEXT.layout ? [["Layout", CONTEXT.layout]] : []
107
123
  };
108
124
  }
109
125
 
126
+ function environment() {
127
+ var rows = [];
128
+ if (CONTEXT.theme) rows.push(["Theme", CONTEXT.theme]);
129
+ return FF.collectEnvironment({
130
+ repository: CONTEXT.repository || CONFIG.repository,
131
+ branch: CONTEXT.branch || CONFIG.baseBranch,
132
+ buildEnv: CONTEXT.jekyllEnv || "",
133
+ rows: rows
134
+ });
135
+ }
136
+
110
137
  function snapshotLogs() {
111
- var store = window.__zer0Feedback;
138
+ var store = window.__fleetFeedback;
112
139
  if (!store || typeof store.snapshot !== "function") return [];
113
140
  try { return store.snapshot(); } catch (err) { return []; }
114
141
  }
115
142
 
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
143
  // --- rendering --------------------------------------------------------
124
144
  function renderContext() {
125
- var rt = runtimeContext();
126
145
  var rows = [
127
146
  ["Page", CONTEXT.title || document.title],
128
- ["URL", rt.href]
147
+ ["URL", location.href]
129
148
  ];
130
149
  if (CONTEXT.sourceUrl) {
131
150
  rows.push(["Source", CONTEXT.path || CONTEXT.sourceUrl]);
@@ -151,7 +170,7 @@
151
170
  if (count > 0) {
152
171
  els.logsWrap.hidden = false;
153
172
  if (els.logsCount) els.logsCount.textContent = String(count);
154
- if (els.logsPreview) els.logsPreview.textContent = formatLogs(state.logs);
173
+ if (els.logsPreview) els.logsPreview.textContent = FF.formatLogs(state.logs);
155
174
  } else {
156
175
  els.logsWrap.hidden = true;
157
176
  }
@@ -195,156 +214,50 @@
195
214
  return ok;
196
215
  }
197
216
 
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 &amp; 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");
217
+ // --- issue assembly (delegated to the shared core) --------------------
218
+
219
+ // The AI review is theme-only enrichment, not part of the fleet contract,
220
+ // so it rides in `extra` — appended to the description with its own heading
221
+ // — rather than forcing a section slot into the shared section order.
222
+ function aiSection() {
223
+ if (!state.analysis) return "";
224
+ var a = state.analysis;
225
+ var out = ["## 🤖 AI review", ""];
226
+ if (a.summary) out.push(a.summary, "");
227
+ var meta = [];
228
+ if (a.priority) meta.push("**Priority:** " + a.priority);
229
+ if (a.severity) meta.push("**Severity:** " + a.severity);
230
+ if (meta.length) out.push(meta.join(" · "), "");
231
+ if (a.recommendation) out.push("**Recommended approach:** " + a.recommendation);
232
+ return out.join("\n").trim();
295
233
  }
296
234
 
297
235
  function buildIssue(opts) {
298
236
  opts = opts || {};
299
237
  var type = typeById(state.typeId);
300
238
  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 };
239
+ var issue = FF.buildIssue({
240
+ type: type,
241
+ description: els.description.value,
242
+ extra: aiSection(),
243
+ page: pageContext(),
244
+ environment: environment(),
245
+ logs: (opts.includeLogs !== false && includeLogs) ? state.logs : [],
246
+ config: {
247
+ labels: CONFIG.defaultLabels || ["page-feedback"],
248
+ assignee: CONFIG.assignee,
249
+ repo: CONFIG.repository
346
250
  }
251
+ });
252
+ // The AI step may sharpen the title and add labels/priority. Those are
253
+ // applied on top of the contract, never in place of it.
254
+ if (state.analysis) {
255
+ if (state.analysis.title) issue.title = state.analysis.title.slice(0, 240);
256
+ var extraLabels = (state.analysis.labels || []).slice();
257
+ if (state.analysis.priority) extraLabels.push("priority:" + state.analysis.priority);
258
+ issue.labels = FF.uniq(issue.labels.concat(extraLabels));
347
259
  }
260
+ return issue;
348
261
  }
349
262
 
350
263
  // --- clipboard --------------------------------------------------------
@@ -376,7 +289,7 @@
376
289
 
377
290
  // --- submit -----------------------------------------------------------
378
291
  function submitViaUrl(issue) {
379
- var built = buildPrefillUrl(issue);
292
+ var built = FF.buildUrl(issue, { repo: CONFIG.repository });
380
293
  // Open WITHOUT the "noopener" feature so we get a real window reference to
381
294
  // detect pop-up blocking, then sever the opener link ourselves.
382
295
  var win = window.open(built.url, "_blank");
@@ -390,10 +303,10 @@
390
303
  });
391
304
  return;
392
305
  }
393
- if (built.dropped.length) {
306
+ if (built.trimmed.length) {
394
307
  // Something was too long for the URL — hand the full body to the clipboard.
395
308
  copyText(issue.body).then(function () {
396
- setStatus("Opened GitHub. The " + built.dropped.join(" and ") +
309
+ setStatus("Opened GitHub. The " + built.trimmed.join(" and ") +
397
310
  " section(s) were too long for the URL, so the full report is on your clipboard — paste it in.", "warn");
398
311
  })["catch"](function () {
399
312
  setStatus("Opened GitHub. Some sections were trimmed to fit the URL.", "warn");
@@ -464,9 +377,9 @@
464
377
  sourceUrl: CONTEXT.sourceUrl, layout: CONTEXT.layout,
465
378
  collection: CONTEXT.collection, repository: CONTEXT.repository, branch: CONTEXT.branch
466
379
  },
467
- environment: runtimeContext(),
468
- logs: includeLogs ? formatLogs(state.logs) : "",
469
- availableLabels: dedupe((CONFIG.defaultLabels || []).concat(collectAllLabels())),
380
+ environment: environment(),
381
+ logs: includeLogs ? FF.formatLogs(state.logs) : "",
382
+ availableLabels: FF.uniq((CONFIG.defaultLabels || []).concat(collectAllLabels())),
470
383
  model: CONFIG.ai.model,
471
384
  maxTokens: CONFIG.ai.maxTokens
472
385
  };
@@ -570,8 +483,19 @@
570
483
  }
571
484
  }
572
485
 
573
- function open(preselectType) {
486
+ /**
487
+ * Open the dialog.
488
+ * @param {string|{type?:string, description?:string, extra?:string}} opts
489
+ * A bare type id is still accepted — window.zer0PageFeedback.open('fix-page')
490
+ * is a published surface — but the object form is what other components use
491
+ * to hand over context the reader should not have to retype: the 404 page
492
+ * passes the URL that failed, an error boundary would pass the stack.
493
+ */
494
+ function open(opts) {
574
495
  if (state.open) return;
496
+ if (typeof opts === "string") opts = { type: opts };
497
+ opts = opts || {};
498
+ var preselectType = opts.type || null;
575
499
  state.open = true;
576
500
  state.lastFocus = document.activeElement;
577
501
  modal.hidden = false;
@@ -584,6 +508,9 @@
584
508
  state.analysis = null;
585
509
  renderContext();
586
510
 
511
+ if (opts.description && els.description) {
512
+ els.description.value = opts.description;
513
+ }
587
514
  var type = preselectType || state.typeId;
588
515
  if (type && typeById(type)) {
589
516
  setType(type);
@@ -591,6 +518,7 @@
591
518
  } else if (els.types.length) {
592
519
  els.types[0].focus();
593
520
  }
521
+ updateValidity();
594
522
  document.addEventListener("keydown", onKeydown, true);
595
523
 
596
524
  if (CONFIG.ai && CONFIG.ai.available && CONFIG.ai.auto && state.typeId) analyze();
@@ -644,12 +572,17 @@
644
572
  if (els.contextCount) els.contextCount.textContent = "(" + (base + withLogs) + ")";
645
573
  }
646
574
 
647
- // Any [data-pf-open] element opens the modal; data-pf-type preselects a type.
575
+ // Any [data-pf-open] element opens the modal; data-pf-type preselects a type
576
+ // and data-pf-description pre-fills the textarea (the 404 page sets it to the
577
+ // URL that failed just before the click reaches here).
648
578
  document.addEventListener("click", function (event) {
649
579
  var trigger = event.target.closest ? event.target.closest("[data-pf-open]") : null;
650
580
  if (!trigger) return;
651
581
  event.preventDefault();
652
- open(trigger.getAttribute("data-pf-type") || null);
582
+ open({
583
+ type: trigger.getAttribute("data-pf-type") || null,
584
+ description: trigger.getAttribute("data-pf-description") || ""
585
+ });
653
586
  });
654
587
 
655
588
  // Update the submit verb to match the configured mode.