jekyll-theme-zer0 1.25.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.
@@ -0,0 +1,252 @@
1
+ {%- comment -%}
2
+ ===================================================================
3
+ PAGE FEEDBACK — "Improve this page" capture widget
4
+ ===================================================================
5
+
6
+ Component: page-feedback
7
+ Path: _includes/components/page-feedback.html
8
+ Purpose: Turn what a reader is looking at into a well-formed GitHub
9
+ issue — request type + description + live page context +
10
+ captured console/error logs — filed with labels that exist.
11
+ Replaces the build-time Copilot dropdown that used to live in
12
+ content/intro.html and baked _data/prompts.yml into <a href>s.
13
+
14
+ Params: none — configured via site.page_feedback (+ site.data.feedback_types)
15
+ Depends on:
16
+ - assets/js/page-feedback.js (behaviour)
17
+ - _sass/components/_page-feedback.scss (styling)
18
+ - _includes/core/console-capture.html (the log buffer, installed in <head>)
19
+ - _data/feedback_types.yml (request-type taxonomy)
20
+ - FAB tokens (_sass/tokens/_spacing.scss, _layers.scss)
21
+
22
+ Layers:
23
+ - Base (always): opens a pre-filled github.com/issues/new form. No token,
24
+ works on any fork / GitHub Pages.
25
+ - AI enrichment (optional): when the chat proxy is deployed
26
+ (ai_chat.proxy_ready), an "Analyze with AI" step sends the capture to
27
+ Claude — via Claude Code OAuth, server-side — to clarify, vet,
28
+ prioritize, and recommend before the issue is filed.
29
+
30
+ Triggers: the global FAB below, plus any element with [data-pf-open]
31
+ elsewhere on the page (e.g. the button in content/intro.html).
32
+ Add data-pf-type="<id>" to preselect a request type.
33
+
34
+ a11y: role=dialog + aria-modal, labelled title, focus trap, Escape to
35
+ close, backdrop click to close, aria-live status region.
36
+ ===================================================================
37
+ {%- endcomment -%}
38
+
39
+ {%- unless site.page_feedback.enabled -%}{%- comment -%} widget disabled {%- endcomment -%}
40
+ {%- else -%}
41
+
42
+ {%- comment -%}
43
+ Booleans aren't evaluated inside assign tags (Liquid stores a truthy string),
44
+ so compute render flags with if-tags. The AI layer needs the chat proxy
45
+ deployed — reuse ai_chat.proxy_ready rather than a second readiness flag.
46
+ {%- endcomment -%}
47
+ {%- assign pf_ai_available = false -%}
48
+ {%- if site.page_feedback.ai.enabled and site.ai_chat.proxy_ready -%}
49
+ {%- assign pf_ai_available = true -%}
50
+ {%- endif -%}
51
+ {%- assign pf_show_fab = false -%}
52
+ {%- if site.page_feedback.fab -%}{%- assign pf_show_fab = true -%}{%- endif -%}
53
+
54
+ {%- comment -%} Repo + file coordinates for the issue body (mirrors content/intro.html). {%- endcomment -%}
55
+ {%- assign pf_repo = site.repository | join: "" -%}
56
+ {%- assign pf_branch = site.branch | default: "main" -%}
57
+ {%- assign pf_source_url = "" -%}
58
+ {%- if pf_repo != "" and page.path -%}
59
+ {%- capture pf_source_url -%}https://github.com/{{ pf_repo }}/blob/{{ pf_branch }}/{{ page.path }}{%- endcapture -%}
60
+ {%- endif -%}
61
+ {%- assign pf_theme = site.theme | default: site.remote_theme | default: "" -%}
62
+
63
+ {%- if pf_show_fab -%}
64
+ <!-- Global feedback FAB (page-invariant; opens the modal) -->
65
+ <button type="button"
66
+ id="pageFeedbackFab"
67
+ class="pf-fab btn shadow-lg position-fixed"
68
+ data-pf-open
69
+ aria-haspopup="dialog"
70
+ aria-controls="pageFeedbackModal"
71
+ title="{{ site.page_feedback.fab_label | default: 'Improve this page' | escape }}"
72
+ aria-label="{{ site.page_feedback.fab_label | default: 'Improve this page' | escape }}">
73
+ <i class="bi {{ site.page_feedback.fab_icon | default: 'bi-megaphone' }}" aria-hidden="true"></i>
74
+ </button>
75
+ {%- endif -%}
76
+
77
+ <!-- Feedback modal (self-contained overlay; controlled by page-feedback.js).
78
+ `position-fixed` is required, not cosmetic: it opts this <body> child out of
79
+ the .zer0-bg-body flattening rule (_sass/theme/_backgrounds.scss) that would
80
+ otherwise force position:relative; z-index:1 and let the cookie banner steal
81
+ the dialog's clicks. -->
82
+ <div class="pf-overlay position-fixed" id="pageFeedbackModal" role="dialog" aria-modal="true"
83
+ aria-labelledby="pfTitle" aria-describedby="pfIntro" hidden>
84
+ <div class="pf-dialog shadow-lg" role="document">
85
+
86
+ <header class="pf-header">
87
+ <div class="pf-header-text">
88
+ <h2 id="pfTitle" class="pf-title h5 mb-1">
89
+ <i class="bi bi-megaphone me-2" aria-hidden="true"></i>Share feedback
90
+ </h2>
91
+ <p id="pfIntro" class="pf-intro text-muted mb-0">
92
+ Report a problem or suggest an improvement — it becomes a GitHub issue
93
+ with the page details attached.
94
+ </p>
95
+ </div>
96
+ <button type="button" class="pf-close btn btn-sm" id="pfClose" aria-label="Close feedback dialog">
97
+ <i class="bi bi-x-lg" aria-hidden="true"></i>
98
+ </button>
99
+ </header>
100
+
101
+ <form id="pfForm" class="pf-body" novalidate>
102
+
103
+ <!-- Step 1 — request type -->
104
+ <fieldset class="pf-field pf-types">
105
+ <legend class="pf-legend">What kind of feedback?</legend>
106
+ {%- assign pf_group = "" -%}
107
+ {%- for t in site.data.feedback_types -%}
108
+ {%- if t.group != pf_group -%}
109
+ {%- unless forloop.first -%}</div>{%- endunless -%}
110
+ <p class="pf-types-group">{{ t.group }}</p>
111
+ <div class="pf-types-grid" role="radiogroup" aria-label="{{ t.group | escape }}">
112
+ {%- assign pf_group = t.group -%}
113
+ {%- endif -%}
114
+ <button type="button"
115
+ class="pf-type"
116
+ role="radio"
117
+ aria-checked="false"
118
+ data-pf-type-id="{{ t.id }}"
119
+ tabindex="{% if forloop.first %}0{% else %}-1{% endif %}">
120
+ <i class="bi {{ t.icon | default: 'bi-chat-dots' }} pf-type-icon" aria-hidden="true"></i>
121
+ <span class="pf-type-text">
122
+ <span class="pf-type-label">{{ t.label }}</span>
123
+ {%- if t.description -%}<span class="pf-type-desc">{{ t.description }}</span>{%- endif -%}
124
+ </span>
125
+ </button>
126
+ {%- if forloop.last -%}</div>{%- endif -%}
127
+ {%- endfor -%}
128
+ </fieldset>
129
+
130
+ <!-- Step 2 — description -->
131
+ <div class="pf-field">
132
+ <label for="pfDescription" class="pf-label">Describe it</label>
133
+ <textarea id="pfDescription" class="form-control" rows="4"
134
+ placeholder="Pick a type above, then tell us what you have in mind."
135
+ aria-describedby="pfDescHelp" required></textarea>
136
+ <p id="pfDescHelp" class="pf-help text-muted">
137
+ Be specific — what you expected, what happened, or what would be better.
138
+ </p>
139
+ </div>
140
+
141
+ <!-- Step 3 — what gets attached -->
142
+ <details class="pf-field pf-context" id="pfContextWrap">
143
+ <summary class="pf-summary">
144
+ <i class="bi bi-paperclip me-1" aria-hidden="true"></i>What gets attached
145
+ <span class="pf-context-count" id="pfContextCount"></span>
146
+ </summary>
147
+ <ul class="pf-context-list" id="pfContextList" aria-live="polite">
148
+ <!-- page title / url / path filled in by JS -->
149
+ </ul>
150
+
151
+ {%- if site.page_feedback.capture_logs -%}
152
+ <div class="pf-logs" id="pfLogsWrap" hidden>
153
+ <label class="pf-check">
154
+ <input type="checkbox" id="pfIncludeLogs" checked>
155
+ Include <span id="pfLogsCount">0</span> captured console/error line(s)
156
+ </label>
157
+ <pre class="pf-logs-preview" id="pfLogsPreview" tabindex="0" aria-label="Captured console logs"></pre>
158
+ <p class="pf-help text-muted mb-0">
159
+ <i class="bi bi-shield-check me-1" aria-hidden="true"></i>
160
+ These come from your browser only and are never sent until you submit.
161
+ Review them and untick the box to leave them out.
162
+ </p>
163
+ </div>
164
+ {%- endif -%}
165
+ </details>
166
+
167
+ {%- if pf_ai_available -%}
168
+ <!-- Step 4 — optional AI review (proxy + Claude Code OAuth) -->
169
+ <div class="pf-field pf-ai" id="pfAiWrap">
170
+ <div class="pf-ai-head">
171
+ <button type="button" class="btn btn-sm btn-outline-primary" id="pfAnalyze">
172
+ <i class="bi bi-robot me-1" aria-hidden="true"></i>Analyze with AI
173
+ </button>
174
+ <span class="pf-ai-hint text-muted">Optional — clarifies, prioritizes, and suggests labels.</span>
175
+ </div>
176
+ <div class="pf-ai-result" id="pfAiResult" aria-live="polite" hidden></div>
177
+ </div>
178
+ {%- endif -%}
179
+
180
+ <p class="pf-status" id="pfStatus" role="status" aria-live="polite"></p>
181
+
182
+ </form>
183
+
184
+ <footer class="pf-footer">
185
+ <button type="button" class="btn btn-link btn-sm pf-copy" id="pfCopy">
186
+ <i class="bi bi-clipboard me-1" aria-hidden="true"></i>Copy details
187
+ </button>
188
+ <div class="pf-footer-primary">
189
+ <button type="button" class="btn btn-secondary btn-sm" id="pfCancel">Cancel</button>
190
+ <button type="submit" form="pfForm" class="btn btn-primary btn-sm" id="pfSubmit" disabled>
191
+ <i class="bi bi-github me-1" aria-hidden="true"></i>
192
+ <span class="pf-submit-label">Open GitHub issue</span>
193
+ </button>
194
+ </div>
195
+ </footer>
196
+
197
+ </div>
198
+ </div>
199
+
200
+ <!-- Widget configuration consumed by assets/js/page-feedback.js -->
201
+ <script id="pageFeedbackConfig" type="application/json">
202
+ {
203
+ "enabled": true,
204
+ "mode": {{ site.page_feedback.mode | default: "url" | jsonify }},
205
+ "endpoint": {{ site.page_feedback.endpoint | default: "/api/feedback" | jsonify }},
206
+ "issueEndpoint": {{ site.page_feedback.issue_endpoint | default: "/api/github/issue" | jsonify }},
207
+ "repository": {{ pf_repo | jsonify }},
208
+ "baseBranch": {{ pf_branch | jsonify }},
209
+ "defaultLabels": {% if site.page_feedback.default_labels %}{{ site.page_feedback.default_labels | jsonify }}{% else %}[]{% endif %},
210
+ "assignee": {{ site.page_feedback.assignee | default: "" | jsonify }},
211
+ "captureLogs": {{ site.page_feedback.capture_logs | default: true | jsonify }},
212
+ "fabLabel": {{ site.page_feedback.fab_label | default: "Improve this page" | jsonify }},
213
+ "ai": {
214
+ "available": {{ pf_ai_available | jsonify }},
215
+ "auto": {{ site.page_feedback.ai.auto | default: false | jsonify }},
216
+ "model": {{ site.page_feedback.ai.model | default: "claude-opus-4-8" | jsonify }},
217
+ "maxTokens": {{ site.page_feedback.ai.max_tokens | default: 1024 }}
218
+ }
219
+ }
220
+ </script>
221
+
222
+ <!-- Request-type taxonomy (from _data/feedback_types.yml) -->
223
+ <script id="pageFeedbackTypes" type="application/json">
224
+ {{ site.data.feedback_types | jsonify }}
225
+ </script>
226
+
227
+ <!-- Live page context (build-time front matter; runtime detail added by JS) -->
228
+ <script id="pageFeedbackContext" type="application/json">
229
+ {
230
+ "title": {{ page.title | default: site.title | jsonify }},
231
+ "url": {{ page.url | absolute_url | jsonify }},
232
+ "path": {{ page.path | default: "" | jsonify }},
233
+ "sourceUrl": {{ pf_source_url | jsonify }},
234
+ "layout": {{ page.layout | default: "" | jsonify }},
235
+ "collection": {{ page.collection | default: "" | jsonify }},
236
+ "categories": {{ page.categories | default: "" | jsonify }},
237
+ "tags": {{ page.tags | default: "" | jsonify }},
238
+ "author": {{ page.author | default: site.author.name | default: "" | jsonify }},
239
+ "date": {{ page.date | date: "%Y-%m-%d" | default: "" | jsonify }},
240
+ "lastmod": {{ page.last_modified_at | default: page.lastmod | date: "%Y-%m-%d" | default: "" | jsonify }},
241
+ "branch": {{ pf_branch | jsonify }},
242
+ "repository": {{ pf_repo | jsonify }},
243
+ "siteUrl": {{ site.url | default: "" | jsonify }},
244
+ "baseurl": {{ site.baseurl | default: "" | jsonify }},
245
+ "theme": {{ pf_theme | jsonify }},
246
+ "jekyllEnv": {{ jekyll.environment | jsonify }}
247
+ }
248
+ </script>
249
+
250
+ <script defer src="{{ '/assets/js/page-feedback.js' | relative_url }}"></script>
251
+
252
+ {%- endunless -%}
@@ -1,13 +1,14 @@
1
1
  <!--
2
2
  file: _includes/content/intro.html
3
- description: Enhanced intro section with share, edit, and Copilot agent session buttons
3
+ description: Enhanced intro section with share, edit, and improve/report buttons
4
4
  path: _includes/content/intro.html
5
5
  features:
6
6
  - Responsive share dropdown with multiple social platforms
7
7
  - Edit on GitHub quick link
8
- - Copilot Agent dropdown: select a prompt template to pre-fill a GitHub issue
9
- - Issue body includes prompt template + page context + environment details
10
- - Prompts sourced from _data/prompts.yml
8
+ - "Improve" button opens the page-feedback widget (capture type + description
9
+ + live page context + console logs GitHub issue). Progressive enhancement:
10
+ it is a real link to the issue form, upgraded by assets/js/page-feedback.js.
11
+ See _includes/components/page-feedback.html.
11
12
  - Responsive flex layout: full-width content block, frosted metadata footer with reading time and action buttons
12
13
  - Balanced padding and typography hierarchy via bd-intro-* classes
13
14
  - Metadata row: author, dates, category, difficulty, tags, view source
@@ -271,64 +272,21 @@
271
272
  </ul>
272
273
  </div>
273
274
 
274
- <!-- Copilot Agent Prompt Dropdown -->
275
- <div class="dropdown">
276
- <button class="btn btn-sm btn-success dropdown-toggle" type="button" id="copilotAgentDropdown" data-bs-toggle="dropdown" aria-expanded="false" title="Open a GitHub issue with a Copilot prompt for this page">
277
- <i class="bi bi-robot" aria-hidden="true"></i>
278
- <span class="d-none d-sm-inline ms-1">Copilot Agent</span>
279
- </button>
280
- <ul class="dropdown-menu dropdown-menu-end copilot-agent-menu" aria-labelledby="copilotAgentDropdown">
281
- {% assign current_group = "" %}
282
- {% for prompt in site.data.prompts %}
283
- {% if prompt.group and prompt.group != current_group %}
284
- {% unless forloop.first %}<li><hr class="dropdown-divider"></li>{% endunless %}
285
- <li><h6 class="dropdown-header">{{ prompt.group }}</h6></li>
286
- {% assign current_group = prompt.group %}
287
- {% endif %}
288
- {% capture issue_title %}[{{ prompt.label }}] {{ page.title }}{% endcapture %}
289
- {% capture issue_body %}{{ prompt.body }}
290
- ---
291
-
292
- ## 📄 Page Context
293
-
294
- | Field | Value |
295
- |---|---|
296
- | **Title** | {{ page.title }} |
297
- | **URL** | {{ site.url }}{{ page.url }} |
298
- | **File** | `{{ file_path }}` |
299
- | **Branch** | `{{ repo_branch }}` |
300
- | **Layout** | `{{ page.layout }}` |
301
- | **Collection** | `{{ page.collection }}` |
302
- | **Author** | {{ page.author | default: site.author.name }} |
303
- | **Date** | {{ page.date }} |
304
- | **Last Modified** | {{ page.lastmod }} |
305
- | **Tags** | {{ page.tags | join: ", " }} |
306
- | **Categories** | {{ page.categories | join: ", " }} |
307
-
308
- ## 🔧 Environment
309
-
310
- | Field | Value |
311
- |---|---|
312
- | **Repository** | `{{ site.repository }}` |
313
- | **Site URL** | {{ site.url }} |
314
- | **Jekyll Env** | `{{ jekyll.environment }}` |
315
- | **Theme** | `{{ site.theme | default: site.remote_theme }}` |
316
- | **Base URL** | `{{ site.baseurl }}` |
317
- {% endcapture %}
318
- <li>
319
- <a class="dropdown-item" href="https://github.com/{{ site.repository }}/issues/new?assignees=copilot&labels=ai-agent&title={{ issue_title | url_encode }}&body={{ issue_body | url_encode }}" target="_blank" rel="noopener noreferrer">
320
- <span class="copilot-agent-item">
321
- <i class="bi {{ prompt.icon }} copilot-agent-item__icon" aria-hidden="true"></i>
322
- <span class="copilot-agent-item__text">
323
- <span class="copilot-agent-item__label">{{ prompt.label }}</span>
324
- {% if prompt.description %}<small class="copilot-agent-item__desc">{{ prompt.description }}</small>{% endif %}
325
- </span>
326
- </span>
327
- </a>
328
- </li>
329
- {% endfor %}
330
- </ul>
331
- </div>
275
+ <!-- Improve / Report opens the page-feedback widget (assets/js/page-feedback.js).
276
+ Progressive enhancement: this is a real link to the issue form, so it
277
+ works without JavaScript; page-feedback.js intercepts [data-pf-open]
278
+ clicks and opens the richer capture modal (context + logs + AI) instead. -->
279
+ {% if site.page_feedback.enabled %}
280
+ <a class="btn btn-sm btn-success bd-intro-action-link"
281
+ href="https://github.com/{{ site.repository }}/issues/new?template=page_improvement.yml&labels=page-feedback"
282
+ data-pf-open
283
+ target="_blank"
284
+ rel="noopener noreferrer"
285
+ title="Report a problem or suggest an improvement for this page">
286
+ <i class="bi bi-megaphone" aria-hidden="true"></i>
287
+ <span class="d-none d-sm-inline ms-1">Improve</span>
288
+ </a>
289
+ {% endif %}
332
290
 
333
291
  <!-- Edit on GitHub Button -->
334
292
  <a href="https://github.com/{{ site.repository }}/blob/{{ repo_branch }}/{{ file_path }}"
@@ -0,0 +1,89 @@
1
+ {%- comment -%}
2
+ ===================================================================
3
+ CONSOLE CAPTURE — early, tiny console/error ring buffer
4
+ ===================================================================
5
+
6
+ File: console-capture.html
7
+ Path: _includes/core/console-capture.html
8
+ Purpose: Capture a capped, in-memory buffer of console output and
9
+ uncaught errors, installed HIGH in <head> — before the
10
+ theme's deferred scripts run — so the page-feedback widget
11
+ (assets/js/page-feedback.js) can attach the logs and JS
12
+ errors that led up to a report.
13
+
14
+ Renders: only when page_feedback.enabled AND page_feedback.capture_logs.
15
+
16
+ Privacy: in-memory only, never persisted, never sent anywhere until the
17
+ user opens the feedback modal, reviews the captured logs, and
18
+ chooses to submit. Any single entry is truncated; the buffer is
19
+ a fixed-size ring (page_feedback.log_limit).
20
+
21
+ Depends on: nothing — pure inline ES5, GitHub Pages-safe (no CDN).
22
+ ===================================================================
23
+ {%- endcomment -%}
24
+ {%- if site.page_feedback.enabled and site.page_feedback.capture_logs -%}
25
+ <script>
26
+ /* page-feedback: early console/error capture — see _includes/core/console-capture.html */
27
+ (function () {
28
+ "use strict";
29
+ if (window.__zer0Feedback && window.__zer0Feedback.installed) { return; }
30
+
31
+ var LIMIT = {{ site.page_feedback.log_limit | default: 40 | jsonify }};
32
+ var MAX_ENTRY = 600; // truncate any single entry so the buffer stays small
33
+ var start = (window.performance && performance.now) ? null : Date.now();
34
+
35
+ var buffer = [];
36
+ function now() { return start === null ? Math.round(performance.now()) : (Date.now() - start); }
37
+
38
+ function stringify(value) {
39
+ if (typeof value === "string") { return value; }
40
+ if (value instanceof Error) {
41
+ return (value.name || "Error") + ": " + (value.message || "") + (value.stack ? "\n" + value.stack : "");
42
+ }
43
+ try { return JSON.stringify(value); } catch (e) { return String(value); }
44
+ }
45
+
46
+ function push(level, args) {
47
+ try {
48
+ var parts = [];
49
+ for (var i = 0; i < args.length; i++) { parts.push(stringify(args[i])); }
50
+ var text = parts.join(" ");
51
+ if (text.length > MAX_ENTRY) { text = text.slice(0, MAX_ENTRY) + "…"; }
52
+ buffer.push({ t: now(), level: level, text: text });
53
+ while (buffer.length > LIMIT) { buffer.shift(); }
54
+ } catch (e) { /* logging must never break the page */ }
55
+ }
56
+
57
+ // Wrap console methods, preserving passthrough to the real console.
58
+ if (window.console) {
59
+ ["log", "info", "warn", "error", "debug"].forEach(function (level) {
60
+ var original = console[level] ? console[level].bind(console) : null;
61
+ console[level] = function () {
62
+ push(level, arguments);
63
+ if (original) { original.apply(null, arguments); }
64
+ };
65
+ });
66
+ }
67
+
68
+ // Uncaught errors and unhandled promise rejections.
69
+ window.addEventListener("error", function (e) {
70
+ if (!e) { return; }
71
+ var where = e.filename ? " (" + e.filename + ":" + (e.lineno || 0) + ":" + (e.colno || 0) + ")" : "";
72
+ push("error", [(e.message || "Script error") + where]);
73
+ });
74
+ window.addEventListener("unhandledrejection", function (e) {
75
+ var reason = e && e.reason;
76
+ var msg = reason && reason.message ? reason.message : stringify(reason);
77
+ push("error", ["Unhandled promise rejection: " + msg]);
78
+ });
79
+
80
+ window.__zer0Feedback = {
81
+ installed: true,
82
+ logs: buffer,
83
+ /** Snapshot the buffer (newest last) for the widget to read. */
84
+ snapshot: function () { return buffer.slice(); },
85
+ clear: function () { buffer.length = 0; }
86
+ };
87
+ })();
88
+ </script>
89
+ {%- endif -%}
@@ -34,6 +34,10 @@
34
34
  <!-- Only load in production builds (JEKYLL_ENV=production); the include also guards against dev hostnames -->
35
35
  {% if jekyll.environment == "production" %}{% include analytics/google-tag-manager-head.html %}{% endif %}
36
36
 
37
+ <!-- Page-feedback: early console/error capture — must run before the theme
38
+ scripts below so a report can include the logs that led up to it. -->
39
+ {% include core/console-capture.html %}
40
+
37
41
  <!-- ================================ -->
38
42
  <!-- JAVASCRIPT LIBRARIES -->
39
43
  <!-- ================================ -->
data/_layouts/root.html CHANGED
@@ -125,6 +125,9 @@
125
125
  <!-- AI Chat Assistant -->
126
126
  {%- include components/ai-chat.html -%}
127
127
 
128
+ <!-- Page-feedback capture widget ("Improve this page" → GitHub issue) -->
129
+ {%- include components/page-feedback.html -%}
130
+
128
131
  <!-- JavaScript dependencies and custom scripts -->
129
132
  <div>
130
133
  <!-- Search functionality -->