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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1143 -13
- data/_data/README.md +2 -0
- data/_data/ai.yml +5 -3
- data/_data/ai_pricing.yml +36 -0
- data/_data/backlog.yml +507 -2
- data/_data/consumers.yml +157 -9
- data/_data/features.yml +303 -20
- data/_data/feedback_types.yml +17 -12
- data/_data/i18n/fr.yml +12 -7
- data/_data/i18n/manifest.yml +39 -12
- data/_data/ingredient_densities.yml +122 -0
- data/_data/landing.yml +5 -2
- data/_data/navigation/main.yml +16 -0
- data/_data/navigation/quickstart.yml +4 -0
- data/_data/recipe_courses.yml +64 -0
- data/_data/site_builder.yml +874 -0
- data/_data/theme-manifest.yml +160 -124
- data/_data/ui-text.yml +26 -0
- data/_includes/README.md +26 -2
- data/_includes/analytics/posthog.html +2 -2
- data/_includes/components/admin-links.html +2 -2
- data/_includes/components/admin-tabs.html +2 -2
- data/_includes/components/ai-chat.html +14 -11
- data/_includes/components/analytics-dashboard.html +8 -8
- data/_includes/components/author-bio.html +1 -1
- data/_includes/components/author-card.html +10 -2
- data/_includes/components/author-eeat.html +4 -4
- data/_includes/components/background-customizer.html +10 -10
- data/_includes/components/background-image.html +114 -0
- data/_includes/components/background-settings.html +28 -15
- data/_includes/components/collection-manager.html +5 -5
- data/_includes/components/component-showcase.html +13 -13
- data/_includes/components/config-editor.html +12 -12
- data/_includes/components/config-viewer.html +8 -8
- data/_includes/components/cookie-consent.html +15 -15
- data/_includes/components/cta-button.html +7 -2
- data/_includes/components/dev-shortcuts.html +7 -7
- data/_includes/components/env-dashboard.html +8 -8
- data/_includes/components/env-switcher.html +9 -9
- data/_includes/components/feature-card.html +2 -2
- data/_includes/components/halfmoon.html +2 -2
- data/_includes/components/info-section.html +42 -37
- data/_includes/components/js-cdn.html +15 -15
- data/_includes/components/language-toggle.html +168 -21
- data/_includes/components/mermaid.html +72 -435
- data/_includes/components/nanobar.html +5 -5
- data/_includes/components/nav-editor.html +2 -2
- data/_includes/components/nav-export.html +2 -2
- data/_includes/components/nav-overview.html +2 -2
- data/_includes/components/page-feedback.html +45 -30
- data/_includes/components/page-views-init.html +55 -0
- data/_includes/components/page-views.html +33 -0
- data/_includes/components/post-card.html +22 -22
- data/_includes/components/post-type-badge.html +2 -2
- data/_includes/components/powered-by.html +2 -2
- data/_includes/components/preview-image.html +6 -0
- data/_includes/components/quick-index.html +2 -2
- data/_includes/components/recipe-card.html +67 -0
- data/_includes/components/recipe-duration.html +50 -0
- data/_includes/components/recipe-grams.html +58 -0
- data/_includes/components/recipe-index.html +96 -0
- data/_includes/components/recipe-ingredients.html +90 -0
- data/_includes/components/recipe-meta.html +96 -0
- data/_includes/components/recipe-nutrition.html +57 -0
- data/_includes/components/recipe-qty.html +73 -0
- data/_includes/components/recipe-ratio.html +151 -0
- data/_includes/components/recipe-scaler.html +73 -0
- data/_includes/components/recipe-steps.html +86 -0
- data/_includes/components/recipe-temp.html +45 -0
- data/_includes/components/search-modal.html +29 -4
- data/_includes/components/searchbar.html +2 -2
- data/_includes/components/shortcuts-modal.html +3 -0
- data/_includes/components/svg-background.html +2 -2
- data/_includes/components/theme-customizer.html +2 -2
- data/_includes/components/theme-info.html +14 -7
- data/_includes/components/theme-preview-gallery.html +22 -22
- data/_includes/content/giscus.html +2 -2
- data/_includes/content/intro.html +8 -8
- data/_includes/content/jsonld-faq.html +2 -2
- data/_includes/content/jsonld-software.html +24 -5
- data/_includes/content/seo.html +4 -4
- data/_includes/content/sitemap.html +27 -27
- data/_includes/content/toc.html +183 -183
- data/_includes/core/branding.html +6 -6
- data/_includes/core/console-capture.html +32 -74
- data/_includes/core/favicon.html +49 -7
- data/_includes/core/footer-fabs.html +17 -3
- data/_includes/core/footer.html +49 -34
- data/_includes/core/head.html +110 -86
- data/_includes/core/header.html +76 -54
- data/_includes/docs/bootstrap-docs.html +8 -8
- data/_includes/landing/landing-install-cards.html +2 -2
- data/_includes/landing/landing-quick-links.html +1 -1
- data/_includes/navigation/admin-nav.html +2 -2
- data/_includes/navigation/nav-tree.html +8 -8
- data/_includes/navigation/navbar.html +12 -12
- data/_includes/navigation/section-sidebar.html +109 -27
- data/_includes/navigation/sidebar-config.html +36 -2
- data/_includes/navigation/sidebar-left.html +17 -16
- data/_includes/navigation/sidebar-right.html +8 -7
- data/_includes/obsidian/full-graph.html +2 -2
- data/_includes/setup/claude-session.html +72 -0
- data/_includes/setup/prereq-checklist.html +90 -0
- data/_includes/setup/wizard.html +924 -222
- data/_includes/stats/stats-categories.html +8 -8
- data/_includes/stats/stats-header.html +14 -14
- data/_includes/stats/stats-metrics.html +14 -14
- data/_includes/stats/stats-no-data.html +12 -12
- data/_includes/stats/stats-overview.html +6 -6
- data/_includes/stats/stats-tags.html +8 -8
- data/_layouts/404.html +38 -24
- data/_layouts/README.md +2 -0
- data/_layouts/admin.html +24 -24
- data/_layouts/article.html +43 -33
- data/_layouts/author.html +20 -20
- data/_layouts/authors.html +2 -2
- data/_layouts/book-abc.html +12 -12
- data/_layouts/book-story.html +15 -15
- data/_layouts/book.html +12 -12
- data/_layouts/collection.html +33 -33
- data/_layouts/cookbook.html +88 -0
- data/_layouts/default.html +31 -28
- data/_layouts/home.html +23 -23
- data/_layouts/index.html +10 -10
- data/_layouts/landing.html +17 -17
- data/_layouts/news.html +44 -44
- data/_layouts/note.html +38 -38
- data/_layouts/notebook.html +34 -34
- data/_layouts/recipe.html +274 -0
- data/_layouts/root.html +92 -55
- data/_layouts/section.html +62 -33
- data/_layouts/setup.html +3 -3
- data/_layouts/sitemap-collection.html +49 -49
- data/_layouts/stats.html +40 -40
- data/_layouts/tag.html +12 -12
- data/_layouts/welcome.html +21 -21
- data/_sass/components/_callout.scss +1 -1
- data/_sass/components/_footer.scss +37 -1
- data/_sass/components/_mermaid.scss +375 -0
- data/_sass/components/_page-views.scss +36 -0
- data/_sass/components/_recipe.scss +506 -0
- data/_sass/components/_setup-wizard.scss +764 -0
- data/_sass/components/_ui-enhancements.scss +6 -6
- data/_sass/core/_navbar.scss +261 -46
- data/_sass/layouts/_landing.scss +2 -2
- data/_sass/layouts/_navbar-extras.scss +14 -4
- data/_sass/tokens/_color.scss +6 -0
- data/_sass/tokens/_index.scss +2 -0
- data/_sass/tokens/_radius.scss +21 -0
- data/_sass/tokens/_typography.scss +4 -0
- data/_sass/utilities/_focus.scss +14 -0
- data/assets/css/main.scss +4 -0
- data/assets/js/ai-chat.js +47 -5
- data/assets/js/fleet-feedback-capture.js +124 -0
- data/assets/js/fleet-feedback.js +853 -0
- data/assets/js/mermaid-diagrams.js +1267 -0
- data/assets/js/modules/navigation/config.js +9 -6
- data/assets/js/modules/navigation/navbar.js +55 -0
- data/assets/js/modules/navigation/scroll-spy.js +315 -80
- data/assets/js/modules/theme/appearance.js +8 -2
- data/assets/js/obsidian-wiki-links.js +8 -3
- data/assets/js/page-feedback.js +125 -192
- data/assets/js/page-views.js +372 -0
- data/assets/js/recipe-scaler.js +501 -0
- data/assets/js/search-modal.js +36 -0
- data/assets/js/setup-wizard.js +2279 -226
- data/assets/js/site-builder.js +1834 -0
- data/assets/js/ui-enhancements.js +11 -3
- data/scripts/README.md +44 -0
- data/scripts/ai/README.md +38 -0
- data/scripts/ai/api_call.rb +124 -0
- data/scripts/ai/usage.rb +314 -0
- data/scripts/ai/usage_report.rb +225 -0
- data/scripts/bin/audit-consumer +39 -7
- data/scripts/bin/giscus-discussions +213 -14
- data/scripts/bin/manifest +35 -12
- data/scripts/ci/agent_review_result.py +164 -0
- data/scripts/ci/test_agent_review_result.py +172 -0
- data/scripts/ci/test_visual_evidence_autogen.py +341 -0
- data/scripts/ci/visual_evidence_autogen.py +1060 -0
- data/scripts/content-review.rb +20 -1
- data/scripts/design-system-check.rb +170 -0
- data/scripts/lib/audit.sh +42 -2
- data/scripts/lint-liquid-raw.rb +137 -0
- data/scripts/test/integration/mermaid +22 -8
- data/scripts/test/lib/run_tests.sh +3 -1
- data/scripts/test/lib/test_agent_review_result.sh +27 -0
- data/scripts/test/lib/test_visual_evidence_autogen.sh +24 -0
- data/scripts/translate.rb +94 -16
- metadata +48 -2
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* fleet-feedback v0.2.0 — the universal "Improve this page" widget.
|
|
3
|
+
* Source: bamr87/bamr87 templates/feedback/ (spec: specs/FEEDBACK.md, UPS-FB).
|
|
4
|
+
*
|
|
5
|
+
* Zero dependencies. One file, three layers, each usable on its own:
|
|
6
|
+
*
|
|
7
|
+
* 1. the console/error capture buffer (byte-identical to capture.js)
|
|
8
|
+
* 2. window.FleetFeedbackCore the issue contract, as pure functions
|
|
9
|
+
* 3. <fleet-feedback> the default shadow-DOM dialog + FAB
|
|
10
|
+
*
|
|
11
|
+
* A host with its own UI — the zer0-mistakes Jekyll theme's Bootstrap modal —
|
|
12
|
+
* loads this file for layers 1 and 2 and never writes the tag, so both widgets
|
|
13
|
+
* file byte-identical issues. Everything else writes the tag and gets layer 3.
|
|
14
|
+
*
|
|
15
|
+
* <script src="/assets/js/fleet-feedback-capture.js"></script> <!-- in <head> -->
|
|
16
|
+
* <script src="/assets/js/fleet-feedback.js" defer></script> <!-- before </body> -->
|
|
17
|
+
* <fleet-feedback repo="owner/name" branch="main" source="pages/about.md"></fleet-feedback>
|
|
18
|
+
*
|
|
19
|
+
* Attributes (all optional except repo): repo, branch, source, route,
|
|
20
|
+
* page-title, labels (csv markers, default "page-feedback"), assignee (default
|
|
21
|
+
* "copilot"; "" disables), mode ("url" | "proxy" | "postmessage"), endpoint,
|
|
22
|
+
* capture-logs, log-limit (40), fab, label, env, app-version, types (URL of a
|
|
23
|
+
* JSON array). Inline taxonomy: <script type="application/json"> child.
|
|
24
|
+
*
|
|
25
|
+
* JS API: window.FleetFeedback.open({ type, description, extra })
|
|
26
|
+
* window.FleetFeedback.breadcrumb('net', 'GET /api/x -> 502')
|
|
27
|
+
* window.FleetFeedbackCore.buildIssue({...}) -> { title, body, labels }
|
|
28
|
+
* any element with [data-fleet-feedback-open] (+ data-type) opens it.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/* ---------------------------------------------------------------------- */
|
|
32
|
+
/* 1. Console + error capture. */
|
|
33
|
+
/* */
|
|
34
|
+
/* This block is BYTE-IDENTICAL to capture.js, which exists so a host can */
|
|
35
|
+
/* install the buffer high in <head> without paying for the whole widget */
|
|
36
|
+
/* (tests/contract.test.mjs fails if the two ever diverge). Loading both */
|
|
37
|
+
/* is the intended install: the early copy wins, this one is a no-op. */
|
|
38
|
+
/* ---------------------------------------------------------------------- */
|
|
39
|
+
(function (root) {
|
|
40
|
+
// Whether the buffer already existed tells us whether capture.js ran first.
|
|
41
|
+
// If it did not, capture starts now — after page load — and the dialog says
|
|
42
|
+
// so rather than implying it saw the errors it structurally could not.
|
|
43
|
+
root.__fleetFeedbackWasEarly = !!(root.__fleetFeedback && root.__fleetFeedback.installed);
|
|
44
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
45
|
+
|
|
46
|
+
/* fleet-feedback:capture:begin — byte-identical in fleet-feedback.js (tests/contract.test.mjs asserts it) */
|
|
47
|
+
(function (root) {
|
|
48
|
+
'use strict';
|
|
49
|
+
|
|
50
|
+
var VERSION = '0.2.0';
|
|
51
|
+
var LIMIT = 40;
|
|
52
|
+
var MAX_ENTRY = 600;
|
|
53
|
+
|
|
54
|
+
var g = (root.__fleetFeedback = root.__fleetFeedback || {
|
|
55
|
+
logs: [],
|
|
56
|
+
limit: LIMIT,
|
|
57
|
+
installed: false,
|
|
58
|
+
version: VERSION
|
|
59
|
+
});
|
|
60
|
+
if (g.installed) { return; }
|
|
61
|
+
|
|
62
|
+
// Secrets end up in console output far more often than anyone expects — an
|
|
63
|
+
// Authorization header logged by a fetch wrapper, a signed URL in a 403, a
|
|
64
|
+
// user's address in a validation message. Redact BEFORE the line enters the
|
|
65
|
+
// buffer, so it can never be previewed, copied, or filed.
|
|
66
|
+
var SCRUB = [
|
|
67
|
+
// The credential can carry a scheme word of its own ("Authorization: Bearer
|
|
68
|
+
// <token>"), and a rule that stops at the scheme leaves the secret in the
|
|
69
|
+
// buffer — which is exactly what the first version of this did.
|
|
70
|
+
[/\b(authorization|proxy-authorization|bearer|token|api[-_]?key|access[-_]?token|refresh[-_]?token|client[-_]?secret|secret|password|passwd)(\s*[:=]\s*|\s+)((?:bearer|basic|digest|token)\s+)?([^\s,;"'&]{6,})/gi, '$1$2$3***'],
|
|
71
|
+
[/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+/g, '***jwt***'],
|
|
72
|
+
[/\bgh[pousr]_[A-Za-z0-9]{16,}/g, '***github-token***'],
|
|
73
|
+
[/\bsk-[A-Za-z0-9_-]{16,}/g, '***api-key***'],
|
|
74
|
+
[/\b[\w.+-]+@[\w-]+\.[\w.-]{2,}\b/g, '***@***']
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
function redact(text) {
|
|
78
|
+
var out = String(text == null ? '' : text);
|
|
79
|
+
for (var i = 0; i < SCRUB.length; i++) {
|
|
80
|
+
out = out.replace(SCRUB[i][0], SCRUB[i][1]);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function stringify(value) {
|
|
86
|
+
if (typeof value === 'string') { return value; }
|
|
87
|
+
if (value instanceof Error) {
|
|
88
|
+
return (value.name || 'Error') + ': ' + (value.message || '') +
|
|
89
|
+
(value.stack ? '\n' + String(value.stack).split('\n').slice(1, 4).join('\n') : '');
|
|
90
|
+
}
|
|
91
|
+
try { return JSON.stringify(value); } catch (e) { return String(value); }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function push(level, parts) {
|
|
95
|
+
try {
|
|
96
|
+
var line = redact(parts.map(stringify).join(' '));
|
|
97
|
+
if (line.length > MAX_ENTRY) { line = line.slice(0, MAX_ENTRY) + '…'; }
|
|
98
|
+
g.logs.push({ t: new Date().toISOString(), level: level, msg: line });
|
|
99
|
+
while (g.logs.length > g.limit) { g.logs.shift(); }
|
|
100
|
+
} catch (e) { /* capture must never break the page it is watching */ }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Only warn/error are hooked. console.log is noise at this budget — 40 lines
|
|
104
|
+
// of debug chatter push the actual failure out of a ring this size.
|
|
105
|
+
if (root.console) {
|
|
106
|
+
['warn', 'error'].forEach(function (level) {
|
|
107
|
+
var original = root.console[level];
|
|
108
|
+
root.console[level] = function () {
|
|
109
|
+
push(level, Array.prototype.slice.call(arguments));
|
|
110
|
+
if (original) { return original.apply(root.console, arguments); }
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (root.addEventListener) {
|
|
116
|
+
root.addEventListener('error', function (e) {
|
|
117
|
+
if (!e) { return; }
|
|
118
|
+
var where = e.filename ? ' (' + e.filename + ':' + (e.lineno || 0) + ':' + (e.colno || 0) + ')' : '';
|
|
119
|
+
push('error', [(e.message || 'Uncaught error') + where]);
|
|
120
|
+
});
|
|
121
|
+
root.addEventListener('unhandledrejection', function (e) {
|
|
122
|
+
var reason = e && e.reason;
|
|
123
|
+
push('error', ['Unhandled promise rejection:', reason && reason.message ? reason.message : reason]);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
g.version = VERSION;
|
|
128
|
+
g.installed = true;
|
|
129
|
+
g.redact = redact;
|
|
130
|
+
/**
|
|
131
|
+
* Push a structured entry. An app's fetch wrapper calls this on a failed
|
|
132
|
+
* request so the report carries the API call that broke, not just the
|
|
133
|
+
* exception it surfaced as:
|
|
134
|
+
* FF.breadcrumb('net', 'GET /api/analysis → 502 (1243ms) req=abc123')
|
|
135
|
+
*/
|
|
136
|
+
g.breadcrumb = function (level, message) { push(level || 'net', [message]); };
|
|
137
|
+
g.snapshot = function () { return g.logs.slice(); };
|
|
138
|
+
g.clear = function () { g.logs.length = 0; };
|
|
139
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
140
|
+
/* fleet-feedback:capture:end */
|
|
141
|
+
|
|
142
|
+
(function (root) {
|
|
143
|
+
if (root.__fleetFeedback) { root.__fleetFeedback.late = !root.__fleetFeedbackWasEarly; }
|
|
144
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
145
|
+
|
|
146
|
+
/* ---------------------------------------------------------------------- */
|
|
147
|
+
/* 2. FleetFeedbackCore — the issue contract, as pure functions. */
|
|
148
|
+
/* */
|
|
149
|
+
/* Everything here takes an explicit input object and returns strings. No */
|
|
150
|
+
/* DOM, no globals, no I/O — so it runs under Node in the contract tests, */
|
|
151
|
+
/* and so a host with its own UI (the zer0-mistakes theme's Bootstrap */
|
|
152
|
+
/* modal) can produce a byte-identical issue by calling the same builder */
|
|
153
|
+
/* instead of reimplementing it. Two UIs, one contract. */
|
|
154
|
+
/* Spec: specs/FEEDBACK.md UPS-FB-20..25. */
|
|
155
|
+
/* ---------------------------------------------------------------------- */
|
|
156
|
+
(function (root) {
|
|
157
|
+
'use strict';
|
|
158
|
+
|
|
159
|
+
var VERSION = '0.2.0';
|
|
160
|
+
|
|
161
|
+
// Pre-filled github.com/issues/new URLs stop working reliably somewhere near
|
|
162
|
+
// 8k (browser and GitHub both weigh in). Budget below that and trim to fit.
|
|
163
|
+
var URL_BUDGET = 7000;
|
|
164
|
+
var SECTION_ORDER = ['description', 'context', 'environment', 'logs', 'directive', 'footer'];
|
|
165
|
+
// Dropped in this order when the URL is over budget: logs are reproducible,
|
|
166
|
+
// the directive is regenerable from the type, the environment table is the
|
|
167
|
+
// last thing an agent needs. Description and page context never go.
|
|
168
|
+
var TRIM_ORDER = ['logs', 'directive', 'environment'];
|
|
169
|
+
var MARKER_RE = /<!-- fleet-feedback v1 type=([a-z0-9-]+) -->/;
|
|
170
|
+
|
|
171
|
+
/* Request-type taxonomy — mirrors templates/feedback/feedback_types.yml.
|
|
172
|
+
Type labels come from the fleet issue-pipeline set (_data/fleet.yml
|
|
173
|
+
issue_pipeline.labels.types) plus optional area:*, so a widget-filed issue
|
|
174
|
+
is pipeline-eligible on the next scan. EVERY label must already exist in
|
|
175
|
+
the target repo: GitHub silently drops unknown labels from a prefilled
|
|
176
|
+
URL — no error, no warning, the label is just gone. */
|
|
177
|
+
var DEFAULT_TYPES = [
|
|
178
|
+
{ id: 'fix-page', label: 'Report a problem', group: 'This page', scope: 'page', description: 'A typo, broken link, wrong information, or something rendering badly', labels: ['bug'], agent: true, directive: 'Reproduce and fix the reported defect on the page identified in Page context. Keep the change minimal and surgical; verify with the project build.', placeholder: 'What is wrong, where on the page, and what did you expect instead?' },
|
|
179
|
+
{ id: 'improve-page', label: 'Improve this page', group: 'This page', scope: 'page', description: 'Polish the copy, structure, or presentation', labels: ['docs'], agent: true, directive: 'Act as a content editor for the page in Page context. Tighten copy, fix grammar, improve heading hierarchy — without changing the core message or breaking links.', placeholder: 'What would make this page clearer, tighter, or more useful?' },
|
|
180
|
+
{ id: 'expand-page', label: 'Add missing detail', group: 'This page', scope: 'page', description: 'Add depth, examples, or a section a reader would expect', labels: ['docs'], agent: true, directive: 'Expand the page in Page context with concrete examples, prerequisites, and any expected-but-missing sections. Preserve tone and front matter.', placeholder: 'What is missing? Which examples or sections would help?' },
|
|
181
|
+
{ id: 'update-page', label: 'Flag outdated content', group: 'This page', scope: 'page', description: 'Stale versions, dead links, or old screenshots', labels: ['docs'], agent: true, directive: 'Audit the page in Page context for stale versions, dead links, and outdated screenshots; refresh them and bump lastmod.', placeholder: 'What is out of date? Paste the stale value or link if you can.' },
|
|
182
|
+
{ id: 'accessibility', label: 'Accessibility issue', group: 'This page', scope: 'page', description: 'Contrast, keyboard, screen-reader, or focus problems', labels: ['bug', 'area:a11y'], agent: true, directive: 'Audit the page in Page context against WCAG 2.1 AA for the reported barrier (contrast, keyboard, focus, ARIA, alt text) and propose concrete fixes.', placeholder: 'What barrier did you hit? Which assistive tech or input method?' },
|
|
183
|
+
{ id: 'ui-ux', label: 'UI / UX improvement', group: 'The site', scope: 'site', description: 'A design or usability refinement', labels: ['feature'], agent: true, directive: 'Propose a token-first UI/UX refinement using the page in Page context as the starting example. Cover responsive behaviour, dark mode, and accessibility.', placeholder: 'What feels off, and how might it look or behave instead?' },
|
|
184
|
+
{ id: 'performance', label: 'Performance', group: 'The site', scope: 'site', description: 'Slow load, layout shift, or heavy assets', labels: ['bug', 'area:perf'], agent: true, directive: 'Profile the page in Page context (LCP, CLS, INP), identify the bottleneck, and propose fixes.', placeholder: 'What felt slow? On what device / connection?' },
|
|
185
|
+
{ id: 'feature', label: 'Feature request', group: 'The site', scope: 'site', description: 'Propose a new capability', labels: ['feature'], agent: false, directive: '', placeholder: 'What should it do, and who benefits?' },
|
|
186
|
+
{ id: 'question', label: 'Ask a question', group: 'The site', scope: 'site', description: 'Something unclear — not necessarily a bug', labels: ['question'], agent: false, directive: '', placeholder: 'What are you trying to do, and where did you get stuck?' }
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
/* Escape a value for a Markdown table cell. Backslashes MUST go first:
|
|
190
|
+
escaping "|" to "\|" and only then escaping backslashes would double the
|
|
191
|
+
one we just added and corrupt the row. */
|
|
192
|
+
function cell(value) {
|
|
193
|
+
return String(value == null ? '' : value)
|
|
194
|
+
.replace(/\\/g, '\\\\')
|
|
195
|
+
.replace(/\|/g, '\\|')
|
|
196
|
+
.replace(/\r?\n/g, ' ');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function uniq(list) {
|
|
200
|
+
var seen = {}, out = [];
|
|
201
|
+
(list || []).forEach(function (item) {
|
|
202
|
+
if (item && !Object.prototype.hasOwnProperty.call(seen, item)) { seen[item] = 1; out.push(item); }
|
|
203
|
+
});
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function typeById(types, id) {
|
|
208
|
+
var list = types || DEFAULT_TYPES;
|
|
209
|
+
for (var i = 0; i < list.length; i++) { if (list[i].id === id) { return list[i]; } }
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function formatLogs(logs) {
|
|
214
|
+
return (logs || []).map(function (l) {
|
|
215
|
+
return (l.t || '') + ' [' + (l.level || 'log') + '] ' + (l.msg || l.text || '');
|
|
216
|
+
}).join('\n');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* Rows are [label, value, raw?] triples; falsy values drop the row so an
|
|
220
|
+
absent field is absent rather than an empty cell. `raw` values are already
|
|
221
|
+
markdown (a link) and skip escaping. */
|
|
222
|
+
function table(rows) {
|
|
223
|
+
var body = rows.filter(function (r) { return r && r[1]; }).map(function (r) {
|
|
224
|
+
return '| **' + r[0] + '** | ' + (r[2] ? r[1] : cell(r[1])) + ' |';
|
|
225
|
+
});
|
|
226
|
+
return body.length ? ['| Field | Value |', '|---|---|'].concat(body).join('\n') : '';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function extraRows(rows) {
|
|
230
|
+
return (rows || []).map(function (r) { return [r[0], r[1]]; });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Build the issue. Pure: same input, same bytes, in a browser or in Node.
|
|
235
|
+
*
|
|
236
|
+
* input = {
|
|
237
|
+
* type: taxonomy entry (required)
|
|
238
|
+
* description: the reader's own words
|
|
239
|
+
* extra: appended to the description — an error stack, a missing route
|
|
240
|
+
* page: { title, url, source, sourceUrl, route, lastmod, rows: [[k,v]] }
|
|
241
|
+
* environment: { browser, viewport, dpr, colorScheme, reducedMotion,
|
|
242
|
+
* referrer, repository, branch, buildEnv, capturedAt, rows }
|
|
243
|
+
* logs: [{ t, level, msg }]
|
|
244
|
+
* config: { labels: [marker labels], assignee: 'copilot' }
|
|
245
|
+
* }
|
|
246
|
+
*/
|
|
247
|
+
function buildIssue(input) {
|
|
248
|
+
var inp = input || {};
|
|
249
|
+
var type = inp.type || DEFAULT_TYPES[0];
|
|
250
|
+
var page = inp.page || {};
|
|
251
|
+
var env = inp.environment || {};
|
|
252
|
+
var cfg = inp.config || {};
|
|
253
|
+
var logs = inp.logs || [];
|
|
254
|
+
var desc = String(inp.description || '').trim();
|
|
255
|
+
var s = {};
|
|
256
|
+
|
|
257
|
+
s.description = '## 📝 Description\n\n' + (desc || '_(no description provided)_') +
|
|
258
|
+
(inp.extra ? '\n\n' + inp.extra : '');
|
|
259
|
+
|
|
260
|
+
s.context = '## 📄 Page context\n\n' + table([
|
|
261
|
+
['Page', page.title],
|
|
262
|
+
['URL', page.url],
|
|
263
|
+
['Source', page.sourceUrl && page.source ? '[`' + cell(page.source) + '`](' + page.sourceUrl + ')' : page.source, true],
|
|
264
|
+
['Collection/route', page.route],
|
|
265
|
+
['Last modified', page.lastmod]
|
|
266
|
+
].concat(extraRows(page.rows)));
|
|
267
|
+
|
|
268
|
+
s.environment = '## 🔧 Environment\n\n' + table([
|
|
269
|
+
['Browser', env.browser],
|
|
270
|
+
['Viewport', env.viewport ? env.viewport + ' @' + (env.dpr || 1) + 'x' : ''],
|
|
271
|
+
['Colour scheme', env.colorScheme ? env.colorScheme + (env.reducedMotion ? ', reduced motion' : '') : ''],
|
|
272
|
+
['Referrer', env.referrer],
|
|
273
|
+
['Repository', cfg.repo || env.repository],
|
|
274
|
+
['Branch', env.branch],
|
|
275
|
+
['Build env', env.buildEnv],
|
|
276
|
+
['Captured at', env.capturedAt]
|
|
277
|
+
].concat(extraRows(env.rows)));
|
|
278
|
+
|
|
279
|
+
/* Fenced inside <details> so a 40-line buffer does not bury the report.
|
|
280
|
+
A ``` inside a captured line would close the fence early — neutralise it. */
|
|
281
|
+
s.logs = logs.length
|
|
282
|
+
? '## 🧾 Console & error logs\n\n<details>\n<summary>' + logs.length + ' captured line(s)</summary>\n\n' +
|
|
283
|
+
'```text\n' + formatLogs(logs).replace(/```/g, "'''") + '\n```\n\n</details>'
|
|
284
|
+
: '';
|
|
285
|
+
|
|
286
|
+
s.directive = type.agent && type.directive
|
|
287
|
+
? '## 🤖 Agent directive\n\n' + type.directive
|
|
288
|
+
: '';
|
|
289
|
+
|
|
290
|
+
/* The marker is what makes a filed issue machine-recognisable: the issue
|
|
291
|
+
pipeline's intake tier reads it and skips re-templating a report that is
|
|
292
|
+
already structured. Keep it last and keep it stable. */
|
|
293
|
+
s.footer = '---\n_Filed from ' + (page.url || '') + ' via fleet-feedback v' + VERSION + '._\n' +
|
|
294
|
+
'<!-- fleet-feedback v1 type=' + type.id + ' -->';
|
|
295
|
+
|
|
296
|
+
var labels = uniq((cfg.labels || ['page-feedback']).concat(type.labels || []));
|
|
297
|
+
var assignees = type.agent && cfg.assignee ? [cfg.assignee] : [];
|
|
298
|
+
var title = ('[' + type.label + '] ' + (page.title || '')).trim().slice(0, 240);
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
title: title,
|
|
302
|
+
sections: s,
|
|
303
|
+
body: assemble(s, []),
|
|
304
|
+
labels: labels,
|
|
305
|
+
assignees: assignees,
|
|
306
|
+
marker: '<!-- fleet-feedback v1 type=' + type.id + ' -->',
|
|
307
|
+
type: type
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function assemble(sections, skip) {
|
|
312
|
+
var drop = skip || [];
|
|
313
|
+
return SECTION_ORDER.filter(function (k) {
|
|
314
|
+
return sections[k] && drop.indexOf(k) < 0;
|
|
315
|
+
}).map(function (k) { return sections[k]; }).join('\n\n');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Compose the prefilled issue URL, trimming sections until it fits the
|
|
320
|
+
* budget. Always reports what was dropped so the caller can hand the full
|
|
321
|
+
* body to the clipboard — nothing is ever silently lost (UPS-FB-06).
|
|
322
|
+
*/
|
|
323
|
+
function buildUrl(issue, opts) {
|
|
324
|
+
var o = opts || {};
|
|
325
|
+
var budget = o.budget || URL_BUDGET;
|
|
326
|
+
var base = 'https://github.com/' + (o.repo || '') + '/issues/new';
|
|
327
|
+
var skip = [], url, i = 0;
|
|
328
|
+
|
|
329
|
+
function compose() {
|
|
330
|
+
var p = new URLSearchParams();
|
|
331
|
+
p.set('title', issue.title);
|
|
332
|
+
p.set('body', assemble(issue.sections, skip));
|
|
333
|
+
if (issue.labels.length) { p.set('labels', issue.labels.join(',')); }
|
|
334
|
+
if (issue.assignees.length) { p.set('assignees', issue.assignees.join(',')); }
|
|
335
|
+
return base + '?' + p.toString();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
url = compose();
|
|
339
|
+
while (url.length > budget && i < TRIM_ORDER.length) {
|
|
340
|
+
skip.push(TRIM_ORDER[i++]);
|
|
341
|
+
url = compose();
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
url: url,
|
|
345
|
+
trimmed: skip,
|
|
346
|
+
overBudget: url.length > budget,
|
|
347
|
+
fullBody: assemble(issue.sections, [])
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* Browser-only convenience: everything about the environment the page can
|
|
352
|
+
see for itself. Never includes identity, IP, cookies, or form contents
|
|
353
|
+
(UPS-FB-08). */
|
|
354
|
+
function collectEnvironment(extra) {
|
|
355
|
+
var e = extra || {};
|
|
356
|
+
var mm = typeof window !== 'undefined' && window.matchMedia;
|
|
357
|
+
return {
|
|
358
|
+
browser: typeof navigator !== 'undefined' ? navigator.userAgent : '',
|
|
359
|
+
viewport: typeof window !== 'undefined' ? window.innerWidth + '×' + window.innerHeight : '',
|
|
360
|
+
dpr: (typeof window !== 'undefined' && window.devicePixelRatio) || 1,
|
|
361
|
+
colorScheme: mm && mm('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
|
|
362
|
+
reducedMotion: !!(mm && mm('(prefers-reduced-motion: reduce)').matches),
|
|
363
|
+
referrer: (typeof document !== 'undefined' && document.referrer) || '',
|
|
364
|
+
repository: e.repository || '',
|
|
365
|
+
branch: e.branch || '',
|
|
366
|
+
buildEnv: e.buildEnv || '',
|
|
367
|
+
capturedAt: new Date().toISOString(),
|
|
368
|
+
rows: e.rows || []
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
var Core = {
|
|
373
|
+
VERSION: VERSION,
|
|
374
|
+
URL_BUDGET: URL_BUDGET,
|
|
375
|
+
SECTION_ORDER: SECTION_ORDER,
|
|
376
|
+
TRIM_ORDER: TRIM_ORDER,
|
|
377
|
+
MARKER_RE: MARKER_RE,
|
|
378
|
+
TYPES: DEFAULT_TYPES,
|
|
379
|
+
cell: cell,
|
|
380
|
+
uniq: uniq,
|
|
381
|
+
typeById: typeById,
|
|
382
|
+
formatLogs: formatLogs,
|
|
383
|
+
buildIssue: buildIssue,
|
|
384
|
+
assemble: assemble,
|
|
385
|
+
buildUrl: buildUrl,
|
|
386
|
+
collectEnvironment: collectEnvironment
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
root.FleetFeedbackCore = Core;
|
|
390
|
+
// CommonJS so tests/contract.test.mjs can require() this file directly —
|
|
391
|
+
// the contract is verified against the same bytes the fleet ships.
|
|
392
|
+
if (typeof module !== 'undefined' && module.exports) { module.exports = Core; }
|
|
393
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
394
|
+
|
|
395
|
+
/* ---------------------------------------------------------------------- */
|
|
396
|
+
/* 3. <fleet-feedback> — the default UI. */
|
|
397
|
+
/* */
|
|
398
|
+
/* Shadow DOM so a host page's CSS cannot break it and it cannot break the */
|
|
399
|
+
/* host's; native <dialog> so focus trapping, Escape, and the backdrop are */
|
|
400
|
+
/* the platform's job rather than ours. Renders ONLY where the tag is */
|
|
401
|
+
/* placed — a host that wants its own dialog can load this file purely for */
|
|
402
|
+
/* the capture buffer and FleetFeedbackCore, and never write the tag. */
|
|
403
|
+
/* ---------------------------------------------------------------------- */
|
|
404
|
+
(function (root) {
|
|
405
|
+
'use strict';
|
|
406
|
+
|
|
407
|
+
if (typeof document === 'undefined' || !root.customElements) { return; }
|
|
408
|
+
if (customElements.get('fleet-feedback')) { return; }
|
|
409
|
+
|
|
410
|
+
var Core = root.FleetFeedbackCore;
|
|
411
|
+
var g = root.__fleetFeedback;
|
|
412
|
+
var VERSION = Core.VERSION;
|
|
413
|
+
|
|
414
|
+
function bool(v, d) { return v == null ? d : (v !== 'false' && v !== '0'); }
|
|
415
|
+
function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
|
|
416
|
+
|
|
417
|
+
/* Tokens first, each falling back to the zer0-mistakes/Bootstrap variable and
|
|
418
|
+
then to a literal, so the widget looks native in a fleet app that ships the
|
|
419
|
+
design tokens AND in a bare page that ships nothing. */
|
|
420
|
+
var STYLE = [
|
|
421
|
+
':host{--_fg:var(--fleet-color-ink,var(--zer0-color-ink,var(--bs-body-color,#1b1f23)));',
|
|
422
|
+
'--_bg:var(--fleet-color-bg-elevated,var(--zer0-color-bg-elevated,var(--bs-body-bg,#fff)));',
|
|
423
|
+
'--_muted:var(--fleet-color-ink-muted,var(--zer0-color-ink-muted,#6c757d));',
|
|
424
|
+
'--_border:var(--fleet-color-border,var(--zer0-color-border,#dee2e6));',
|
|
425
|
+
'--_primary:var(--fleet-color-primary,var(--zer0-color-primary,var(--bs-primary,#0d6efd)));',
|
|
426
|
+
'--_radius:var(--fleet-radius-lg,.5rem);--_shadow:var(--fleet-shadow-lg,0 1rem 3rem rgba(0,0,0,.18));',
|
|
427
|
+
'--_focus:var(--fleet-shadow-focus,0 0 0 .25rem rgba(13,110,253,.35));',
|
|
428
|
+
'--_fab-layer:var(--fleet-layer-fab-feedback,1051);--_modal-layer:var(--fleet-layer-feedback-modal,1096);',
|
|
429
|
+
'--_offset:var(--fleet-space-fab-offset,1rem);--_size:var(--fleet-space-fab-size,3.5rem);',
|
|
430
|
+
'font:400 1rem/1.55 var(--fleet-font-sans,system-ui,-apple-system,"Segoe UI",sans-serif);color:var(--_fg)}',
|
|
431
|
+
'*{box-sizing:border-box}',
|
|
432
|
+
'.fab{position:fixed;right:var(--_offset);bottom:calc(var(--_offset) + var(--fleet-space-fab-gap,.75rem) + var(--_size));',
|
|
433
|
+
'z-index:var(--_fab-layer);width:var(--_size);height:var(--_size);border-radius:50%;border:0;background:var(--_primary);',
|
|
434
|
+
'color:#fff;cursor:pointer;box-shadow:var(--_shadow);font-size:1.35rem;display:grid;place-items:center}',
|
|
435
|
+
'.fab:focus-visible,button:focus-visible,textarea:focus-visible,input:focus-visible{outline:0;box-shadow:var(--_focus)}',
|
|
436
|
+
'dialog{z-index:var(--_modal-layer);border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_bg);',
|
|
437
|
+
'color:var(--_fg);padding:0;width:min(40rem,calc(100vw - 2rem));max-height:calc(100vh - 2rem);box-shadow:var(--_shadow)}',
|
|
438
|
+
'dialog::backdrop{background:rgba(0,0,0,.5)}',
|
|
439
|
+
'form{display:flex;flex-direction:column;gap:1rem;padding:1.25rem;max-height:calc(100vh - 2rem);overflow:auto}',
|
|
440
|
+
'header{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem}',
|
|
441
|
+
'h2{margin:0;font-size:1.2rem}.sub{margin:.25rem 0 0;font-size:.85rem;color:var(--_muted)}',
|
|
442
|
+
'.close{background:none;border:0;font-size:1.5rem;line-height:1;cursor:pointer;color:var(--_muted);padding:0 .25rem}',
|
|
443
|
+
'fieldset{border:0;padding:0;margin:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(13rem,1fr));gap:.5rem}',
|
|
444
|
+
'legend,.h{font-weight:600;margin:0 0 .5rem;padding:0;font-size:.95rem}',
|
|
445
|
+
'.type{display:flex;gap:.5rem;align-items:flex-start;border:1px solid var(--_border);border-radius:var(--_radius);padding:.5rem .6rem;cursor:pointer}',
|
|
446
|
+
'.type:has(input:checked){border-color:var(--_primary);box-shadow:inset 0 0 0 1px var(--_primary)}',
|
|
447
|
+
'.type small{display:block;color:var(--_muted);font-size:.78rem;line-height:1.35}',
|
|
448
|
+
'.type input{margin-top:.2rem;flex:none}',
|
|
449
|
+
'textarea{width:100%;min-height:7rem;padding:.6rem;border:1px solid var(--_border);border-radius:var(--_radius);',
|
|
450
|
+
'background:transparent;color:inherit;font:inherit;resize:vertical}',
|
|
451
|
+
'details{border:1px solid var(--_border);border-radius:var(--_radius);padding:.5rem .75rem}',
|
|
452
|
+
'summary{cursor:pointer;font-weight:600;font-size:.9rem}',
|
|
453
|
+
'ul{margin:.5rem 0 0;padding-left:1.15rem;font-size:.82rem;color:var(--_muted)}li{margin:.15rem 0}',
|
|
454
|
+
'pre{margin:.5rem 0 0;max-height:9rem;overflow:auto;font-size:.72rem;white-space:pre-wrap;word-break:break-word;',
|
|
455
|
+
'background:rgba(127,127,127,.08);padding:.5rem;border-radius:var(--_radius)}',
|
|
456
|
+
'.row{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.grow{flex:1 1 8rem}',
|
|
457
|
+
'.check{display:flex;gap:.4rem;align-items:center;margin-top:.5rem;font-size:.85rem}',
|
|
458
|
+
'.btn{border:1px solid var(--_primary);border-radius:var(--_radius);padding:.5rem 1rem;font:inherit;',
|
|
459
|
+
'cursor:pointer;background:var(--_primary);color:#fff}',
|
|
460
|
+
'.btn[disabled]{opacity:.55;cursor:not-allowed}',
|
|
461
|
+
'.btn.ghost{background:transparent;color:var(--_primary)}',
|
|
462
|
+
'.status{font-size:.85rem;color:var(--_muted);min-height:1.2em}',
|
|
463
|
+
'.status[data-kind="warn"]{color:var(--fleet-color-warning,#b45309)}',
|
|
464
|
+
'.status[data-kind="ok"]{color:var(--fleet-color-success,#15803d)}',
|
|
465
|
+
'@media (prefers-reduced-motion:no-preference){dialog[open]{animation:ff-in var(--fleet-motion-base,180ms) var(--fleet-ease,cubic-bezier(.2,0,0,1))}}',
|
|
466
|
+
'@keyframes ff-in{from{opacity:0;transform:translateY(.5rem)}to{opacity:1;transform:none}}'
|
|
467
|
+
].join('');
|
|
468
|
+
|
|
469
|
+
function FleetFeedback() {
|
|
470
|
+
var self = Reflect.construct(HTMLElement, [], FleetFeedback);
|
|
471
|
+
self._types = Core.TYPES;
|
|
472
|
+
self._preset = {};
|
|
473
|
+
return self;
|
|
474
|
+
}
|
|
475
|
+
FleetFeedback.prototype = Object.create(HTMLElement.prototype);
|
|
476
|
+
FleetFeedback.prototype.constructor = FleetFeedback;
|
|
477
|
+
|
|
478
|
+
FleetFeedback.prototype.config = function () {
|
|
479
|
+
var a = this.getAttribute.bind(this);
|
|
480
|
+
return {
|
|
481
|
+
repo: a('repo') || '',
|
|
482
|
+
branch: a('branch') || 'main',
|
|
483
|
+
source: a('source') || '',
|
|
484
|
+
route: a('route') || '',
|
|
485
|
+
pageTitle: a('page-title') || document.title,
|
|
486
|
+
labels: (a('labels') || 'page-feedback').split(',').map(function (s) { return s.trim(); }).filter(Boolean),
|
|
487
|
+
assignee: a('assignee') == null ? 'copilot' : a('assignee'),
|
|
488
|
+
mode: ['proxy', 'postmessage'].indexOf(a('mode')) >= 0 ? a('mode') : 'url',
|
|
489
|
+
endpoint: a('endpoint') || '/api/feedback/issue',
|
|
490
|
+
captureLogs: bool(a('capture-logs'), true),
|
|
491
|
+
logLimit: parseInt(a('log-limit') || '40', 10) || 40,
|
|
492
|
+
fab: bool(a('fab'), true),
|
|
493
|
+
label: a('label') || 'Improve this page',
|
|
494
|
+
env: a('env') || '',
|
|
495
|
+
appVersion: a('app-version') || '',
|
|
496
|
+
typesUrl: a('types') || ''
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
FleetFeedback.prototype.connectedCallback = function () {
|
|
501
|
+
var self = this;
|
|
502
|
+
var cfg = this.config();
|
|
503
|
+
if (g) { g.limit = cfg.logLimit; }
|
|
504
|
+
|
|
505
|
+
var inline = this.querySelector('script[type="application/json"]');
|
|
506
|
+
if (inline) {
|
|
507
|
+
try { this._types = JSON.parse(inline.textContent); }
|
|
508
|
+
catch (e) { console.warn('fleet-feedback: inline types JSON is invalid — using the built-in taxonomy'); }
|
|
509
|
+
} else if (cfg.typesUrl && root.fetch) {
|
|
510
|
+
root.fetch(cfg.typesUrl)
|
|
511
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
512
|
+
.then(function (t) { if (Array.isArray(t) && t.length) { self._types = t; self._renderTypes(); } })
|
|
513
|
+
.catch(function () { /* the built-in taxonomy is the fallback */ });
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
this._render(cfg);
|
|
517
|
+
|
|
518
|
+
// Progressive enhancement, registered once for the document: any
|
|
519
|
+
// [data-fleet-feedback-open] anchor anywhere becomes a dialog trigger, and
|
|
520
|
+
// stays a working link to the issue form when this script never loads.
|
|
521
|
+
if (!root.__fleetFeedbackDelegated) {
|
|
522
|
+
root.__fleetFeedbackDelegated = true;
|
|
523
|
+
document.addEventListener('click', function (e) {
|
|
524
|
+
var t = e.target && e.target.closest && e.target.closest('[data-fleet-feedback-open]');
|
|
525
|
+
if (!t || !root.FleetFeedback) { return; }
|
|
526
|
+
e.preventDefault();
|
|
527
|
+
root.FleetFeedback.open({
|
|
528
|
+
type: t.getAttribute('data-type') || '',
|
|
529
|
+
description: t.getAttribute('data-description') || ''
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
root.FleetFeedback = {
|
|
535
|
+
open: function (o) { self.open(o || {}); },
|
|
536
|
+
close: function () { self.close(); },
|
|
537
|
+
breadcrumb: function (level, message) { if (g && g.breadcrumb) { g.breadcrumb(level, message); } },
|
|
538
|
+
logs: g ? g.logs : [],
|
|
539
|
+
core: Core,
|
|
540
|
+
version: VERSION,
|
|
541
|
+
element: self
|
|
542
|
+
};
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
FleetFeedback.prototype._render = function (cfg) {
|
|
546
|
+
var shadow = this.attachShadow({ mode: 'open' });
|
|
547
|
+
shadow.innerHTML =
|
|
548
|
+
'<style>' + STYLE + '</style>' +
|
|
549
|
+
(cfg.fab
|
|
550
|
+
? '<button type="button" class="fab" aria-haspopup="dialog" aria-controls="ff-dlg" title="' +
|
|
551
|
+
esc(cfg.label) + '" aria-label="' + esc(cfg.label) + '">📣</button>'
|
|
552
|
+
: '') +
|
|
553
|
+
'<dialog id="ff-dlg" aria-labelledby="ff-title">' +
|
|
554
|
+
'<form novalidate>' +
|
|
555
|
+
'<header><div><h2 id="ff-title">' + esc(cfg.label) + '</h2>' +
|
|
556
|
+
'<p class="sub" id="ff-sub">This becomes a GitHub issue on <code>' + esc(cfg.repo) + '</code> with the page details attached.</p></div>' +
|
|
557
|
+
'<button type="button" class="close" id="ff-close" aria-label="Close">×</button></header>' +
|
|
558
|
+
'<div><p class="h" id="ff-tl">What kind of request?</p>' +
|
|
559
|
+
'<fieldset id="ff-types" aria-labelledby="ff-tl"></fieldset></div>' +
|
|
560
|
+
'<div><label class="h" for="ff-desc">Describe it</label>' +
|
|
561
|
+
'<textarea id="ff-desc" name="description" required aria-describedby="ff-help"></textarea>' +
|
|
562
|
+
'<p class="sub" id="ff-help">Be specific — what you expected, what happened, or what would be better.</p></div>' +
|
|
563
|
+
'<details id="ff-ctx-wrap"><summary>What will be attached <span id="ff-ctx-count"></span></summary>' +
|
|
564
|
+
'<ul id="ff-ctx" aria-live="polite"></ul>' +
|
|
565
|
+
'<label class="check"><input type="checkbox" id="ff-logs" checked> ' +
|
|
566
|
+
'<span>Include captured console & error logs (<span id="ff-nlogs">0</span>)</span></label>' +
|
|
567
|
+
'<pre id="ff-logpre" tabindex="0" aria-label="Captured console and error logs" hidden></pre>' +
|
|
568
|
+
'<p class="sub" id="ff-privacy">Captured in your browser only. Nothing is sent until you submit.</p>' +
|
|
569
|
+
'</details>' +
|
|
570
|
+
'<p class="status" id="ff-status" role="status" aria-live="polite"></p>' +
|
|
571
|
+
'<div class="row"><button type="button" class="btn ghost" id="ff-copy">Copy details</button>' +
|
|
572
|
+
'<span class="grow"></span>' +
|
|
573
|
+
'<button type="button" class="btn ghost" id="ff-cancel">Cancel</button>' +
|
|
574
|
+
'<button type="submit" class="btn" id="ff-submit">Open GitHub issue</button></div>' +
|
|
575
|
+
'</form></dialog>';
|
|
576
|
+
|
|
577
|
+
var $ = function (sel) { return shadow.querySelector(sel); };
|
|
578
|
+
this._$ = $;
|
|
579
|
+
this._renderTypes();
|
|
580
|
+
|
|
581
|
+
var self = this;
|
|
582
|
+
if (cfg.fab) { $('.fab').addEventListener('click', function () { self.open({}); }); }
|
|
583
|
+
$('#ff-close').addEventListener('click', function () { self.close(); });
|
|
584
|
+
$('#ff-cancel').addEventListener('click', function () { self.close(); });
|
|
585
|
+
$('#ff-logs').addEventListener('change', function (e) {
|
|
586
|
+
$('#ff-logpre').hidden = !e.target.checked;
|
|
587
|
+
self._renderContextCount();
|
|
588
|
+
});
|
|
589
|
+
$('#ff-desc').addEventListener('input', function () { self._syncSubmit(); });
|
|
590
|
+
$('#ff-copy').addEventListener('click', function () { self.copy(); });
|
|
591
|
+
$('form').addEventListener('submit', function (e) { e.preventDefault(); self.submit(); });
|
|
592
|
+
// <dialog> handles Escape and focus trapping; the backdrop is ours. The
|
|
593
|
+
// click lands on the dialog itself when it hits the ::backdrop area.
|
|
594
|
+
$('#ff-dlg').addEventListener('click', function (e) { if (e.target === $('#ff-dlg')) { self.close(); } });
|
|
595
|
+
$('#ff-dlg').addEventListener('close', function () {
|
|
596
|
+
if (self._opener && self._opener.focus) { self._opener.focus(); }
|
|
597
|
+
});
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
FleetFeedback.prototype._renderTypes = function () {
|
|
601
|
+
if (!this._$) { return; }
|
|
602
|
+
var fs = this._$('#ff-types');
|
|
603
|
+
fs.innerHTML = this._types.map(function (t, i) {
|
|
604
|
+
return '<label class="type"><input type="radio" name="type" value="' + esc(t.id) + '"' +
|
|
605
|
+
(i === 0 ? ' checked' : '') + ' aria-describedby="ff-d-' + esc(t.id) + '">' +
|
|
606
|
+
'<span><strong>' + esc(t.label) + '</strong>' +
|
|
607
|
+
'<small id="ff-d-' + esc(t.id) + '">' + esc(t.description || '') + '</small></span></label>';
|
|
608
|
+
}).join('');
|
|
609
|
+
var self = this;
|
|
610
|
+
fs.addEventListener('change', function () { self._syncType(); });
|
|
611
|
+
this._syncType();
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
FleetFeedback.prototype._type = function () {
|
|
615
|
+
var checked = this._$('input[name=type]:checked');
|
|
616
|
+
return Core.typeById(this._types, checked ? checked.value : '') || this._types[0];
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
FleetFeedback.prototype._syncType = function () {
|
|
620
|
+
var t = this._type();
|
|
621
|
+
if (t) { this._$('#ff-desc').placeholder = t.placeholder || ''; }
|
|
622
|
+
var cfg = this.config();
|
|
623
|
+
var submit = this._$('#ff-submit');
|
|
624
|
+
if (t && t.agent && cfg.assignee) {
|
|
625
|
+
submit.title = 'Files the issue and assigns @' + cfg.assignee;
|
|
626
|
+
} else {
|
|
627
|
+
submit.removeAttribute('title');
|
|
628
|
+
}
|
|
629
|
+
this._syncSubmit();
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
FleetFeedback.prototype._syncSubmit = function () {
|
|
633
|
+
this._$('#ff-submit').disabled = !this._$('#ff-desc').value.trim();
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
FleetFeedback.prototype._logs = function () {
|
|
637
|
+
var cfg = this.config();
|
|
638
|
+
if (!cfg.captureLogs || !g) { return []; }
|
|
639
|
+
return g.snapshot ? g.snapshot() : (g.logs || []).slice();
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
FleetFeedback.prototype._renderContextCount = function () {
|
|
643
|
+
var el = this._$('#ff-ctx-count');
|
|
644
|
+
if (!el) { return; }
|
|
645
|
+
var n = this._$('#ff-ctx').children.length + (this._$('#ff-logs').checked ? 1 : 0);
|
|
646
|
+
el.textContent = '(' + n + ')';
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
FleetFeedback.prototype.page = function (cfg) {
|
|
650
|
+
return {
|
|
651
|
+
title: cfg.pageTitle,
|
|
652
|
+
url: location.href,
|
|
653
|
+
source: cfg.source,
|
|
654
|
+
sourceUrl: cfg.source && cfg.repo
|
|
655
|
+
? 'https://github.com/' + cfg.repo + '/blob/' + cfg.branch + '/' + cfg.source
|
|
656
|
+
: '',
|
|
657
|
+
route: cfg.route || (typeof location !== 'undefined' ? location.pathname : ''),
|
|
658
|
+
lastmod: (document.querySelector('meta[property="article:modified_time"]') || {}).content || ''
|
|
659
|
+
};
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
FleetFeedback.prototype.environment = function (cfg) {
|
|
663
|
+
var rows = [];
|
|
664
|
+
if (cfg.appVersion) { rows.push(['App version', cfg.appVersion]); }
|
|
665
|
+
return Core.collectEnvironment({
|
|
666
|
+
repository: cfg.repo,
|
|
667
|
+
branch: cfg.branch,
|
|
668
|
+
buildEnv: cfg.env,
|
|
669
|
+
rows: rows
|
|
670
|
+
});
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
FleetFeedback.prototype.buildIssue = function () {
|
|
674
|
+
var cfg = this.config();
|
|
675
|
+
return Core.buildIssue({
|
|
676
|
+
type: this._type(),
|
|
677
|
+
description: this._$('#ff-desc').value,
|
|
678
|
+
extra: this._preset.extra || '',
|
|
679
|
+
page: this.page(cfg),
|
|
680
|
+
environment: this.environment(cfg),
|
|
681
|
+
logs: this._$('#ff-logs').checked ? this._logs() : [],
|
|
682
|
+
config: { labels: cfg.labels, assignee: cfg.assignee, repo: cfg.repo }
|
|
683
|
+
});
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
FleetFeedback.prototype.open = function (opts) {
|
|
687
|
+
var cfg = this.config();
|
|
688
|
+
this._preset = opts || {};
|
|
689
|
+
this._opener = document.activeElement;
|
|
690
|
+
|
|
691
|
+
if (this._preset.type) {
|
|
692
|
+
var radio = this._$('input[name=type][value="' + this._preset.type + '"]');
|
|
693
|
+
if (radio) { radio.checked = true; }
|
|
694
|
+
}
|
|
695
|
+
this._syncType();
|
|
696
|
+
if (this._preset.description) { this._$('#ff-desc').value = this._preset.description; }
|
|
697
|
+
|
|
698
|
+
var page = this.page(cfg);
|
|
699
|
+
var items = [
|
|
700
|
+
'Page: ' + page.title,
|
|
701
|
+
'URL: ' + page.url,
|
|
702
|
+
page.source ? 'Source: ' + page.source : '',
|
|
703
|
+
'Browser, viewport, colour scheme, referrer',
|
|
704
|
+
'Repository: ' + cfg.repo + ' @ ' + cfg.branch
|
|
705
|
+
].filter(Boolean);
|
|
706
|
+
this._$('#ff-ctx').innerHTML = items.map(function (s) { return '<li>' + esc(s) + '</li>'; }).join('');
|
|
707
|
+
|
|
708
|
+
var logs = this._logs();
|
|
709
|
+
var logsBox = this._$('#ff-logs');
|
|
710
|
+
logsBox.disabled = logs.length === 0;
|
|
711
|
+
logsBox.checked = logs.length > 0;
|
|
712
|
+
this._$('#ff-nlogs').textContent = String(logs.length);
|
|
713
|
+
this._$('#ff-logpre').textContent = Core.formatLogs(logs);
|
|
714
|
+
this._$('#ff-logpre').hidden = logs.length === 0;
|
|
715
|
+
// Honest about a late start: without capture.js in <head>, everything that
|
|
716
|
+
// failed during page load happened before the hooks existed.
|
|
717
|
+
this._$('#ff-privacy').textContent = (g && g.installed && !g.late)
|
|
718
|
+
? 'Captured in your browser only. Nothing is sent until you submit.'
|
|
719
|
+
: 'Capture started after page load — earlier errors are not included. Nothing is sent until you submit.';
|
|
720
|
+
this._renderContextCount();
|
|
721
|
+
|
|
722
|
+
this._status('');
|
|
723
|
+
this._$('#ff-submit').textContent = cfg.mode === 'url' ? 'Open GitHub issue' : 'File issue';
|
|
724
|
+
this._syncSubmit();
|
|
725
|
+
|
|
726
|
+
var dlg = this._$('#ff-dlg');
|
|
727
|
+
if (!dlg.open) { dlg.showModal(); }
|
|
728
|
+
this._$('#ff-desc').focus();
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
FleetFeedback.prototype.close = function () {
|
|
732
|
+
var dlg = this._$('#ff-dlg');
|
|
733
|
+
if (dlg && dlg.open) { dlg.close(); }
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
FleetFeedback.prototype._status = function (message, kind) {
|
|
737
|
+
var el = this._$('#ff-status');
|
|
738
|
+
el.textContent = message || '';
|
|
739
|
+
if (kind) { el.setAttribute('data-kind', kind); } else { el.removeAttribute('data-kind'); }
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
FleetFeedback.prototype._clipboard = function (text) {
|
|
743
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
744
|
+
return navigator.clipboard.writeText(text);
|
|
745
|
+
}
|
|
746
|
+
return Promise.reject(new Error('clipboard unavailable'));
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
FleetFeedback.prototype.copy = function () {
|
|
750
|
+
var self = this;
|
|
751
|
+
this._clipboard(this.buildIssue().body).then(function () {
|
|
752
|
+
self._status('The full report is on your clipboard.', 'ok');
|
|
753
|
+
}, function () {
|
|
754
|
+
self._status('Could not reach the clipboard — select the log preview and copy manually.', 'warn');
|
|
755
|
+
});
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
FleetFeedback.prototype.submit = function () {
|
|
759
|
+
var cfg = this.config(), self = this;
|
|
760
|
+
if (!cfg.repo) { this._status('fleet-feedback: the repo attribute is missing.', 'warn'); return; }
|
|
761
|
+
if (!this._$('#ff-desc').value.trim()) {
|
|
762
|
+
this._status('Please describe the request first.', 'warn');
|
|
763
|
+
this._$('#ff-desc').focus();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
var issue = this.buildIssue();
|
|
767
|
+
if (cfg.mode === 'postmessage') { return this._submitHost(issue); }
|
|
768
|
+
if (cfg.mode === 'proxy') { return this._submitProxy(issue, cfg); }
|
|
769
|
+
return this._submitUrl(issue, cfg);
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
/* url mode (default): no token anywhere. The reader submits the prefilled
|
|
773
|
+
form under their own GitHub account. */
|
|
774
|
+
FleetFeedback.prototype._submitUrl = function (issue, cfg) {
|
|
775
|
+
var self = this;
|
|
776
|
+
var built = Core.buildUrl(issue, { repo: cfg.repo });
|
|
777
|
+
// No 'noopener' feature, so we get a real window handle and can tell a
|
|
778
|
+
// blocked pop-up from an opened one; the opener link is severed by hand.
|
|
779
|
+
var win = root.open(built.url, '_blank');
|
|
780
|
+
if (win) { try { win.opener = null; } catch (e) { /* cross-origin */ } }
|
|
781
|
+
|
|
782
|
+
if (!win) {
|
|
783
|
+
this._clipboard(built.fullBody).then(function () {
|
|
784
|
+
self._status('Your browser blocked the pop-up. The full report is on your clipboard — open a new issue and paste it in.', 'warn');
|
|
785
|
+
}, function () {
|
|
786
|
+
self._status('Your browser blocked the pop-up. Allow pop-ups for this site and try again.', 'warn');
|
|
787
|
+
});
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (built.trimmed.length) {
|
|
791
|
+
this._clipboard(built.fullBody).then(function () {
|
|
792
|
+
self._status('Opened GitHub. The ' + built.trimmed.join(' and ') +
|
|
793
|
+
' section(s) were too long for the URL — the full report is on your clipboard; paste it over the prefilled body.', 'warn');
|
|
794
|
+
}, function () {
|
|
795
|
+
self._status('Opened GitHub. Some sections were trimmed to fit the URL.', 'warn');
|
|
796
|
+
});
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
this._status('Opened a prefilled GitHub issue in a new tab.', 'ok');
|
|
800
|
+
root.setTimeout(function () { self.close(); }, 1200);
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
/* proxy mode: an endpoint the host owns holds the token server-side. Falls
|
|
804
|
+
back to the always-works url path rather than losing the report. */
|
|
805
|
+
FleetFeedback.prototype._submitProxy = function (issue, cfg) {
|
|
806
|
+
var self = this;
|
|
807
|
+
this._status('Filing…');
|
|
808
|
+
this._$('#ff-submit').disabled = true;
|
|
809
|
+
root.fetch(cfg.endpoint, {
|
|
810
|
+
method: 'POST',
|
|
811
|
+
headers: { 'Content-Type': 'application/json' },
|
|
812
|
+
body: JSON.stringify({
|
|
813
|
+
title: issue.title,
|
|
814
|
+
body: issue.body,
|
|
815
|
+
labels: issue.labels,
|
|
816
|
+
assignees: issue.assignees,
|
|
817
|
+
type: issue.type.id
|
|
818
|
+
})
|
|
819
|
+
}).then(function (r) {
|
|
820
|
+
return r.json().catch(function () { return {}; }).then(function (d) {
|
|
821
|
+
if (!r.ok) { throw new Error((d && d.error) || ('HTTP ' + r.status)); }
|
|
822
|
+
return d;
|
|
823
|
+
});
|
|
824
|
+
}).then(function (d) {
|
|
825
|
+
var url = d.url || d.html_url;
|
|
826
|
+
self._status(url ? 'Filed issue #' + (d.number || '') + ' — ' + url : 'Issue filed.', 'ok');
|
|
827
|
+
root.setTimeout(function () { self.close(); }, 2500);
|
|
828
|
+
}).catch(function (e) {
|
|
829
|
+
self._status('Could not file directly (' + e.message + ') — opening a prefilled form instead…', 'warn');
|
|
830
|
+
self._submitUrl(issue, cfg);
|
|
831
|
+
}).then(function () {
|
|
832
|
+
self._$('#ff-submit').disabled = false;
|
|
833
|
+
});
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
/* postmessage mode: a sandboxed host (VS Code webview) has no window.open
|
|
837
|
+
and no network of its own. Hand the issue to the extension host, which
|
|
838
|
+
files it or opens the URL with vscode.env.openExternal. */
|
|
839
|
+
FleetFeedback.prototype._submitHost = function (issue) {
|
|
840
|
+
var api = root.__fleetFeedbackHost ||
|
|
841
|
+
(typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : null);
|
|
842
|
+
if (!api || typeof api.postMessage !== 'function') {
|
|
843
|
+
this._status('No host bridge available to file this issue.', 'warn');
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
api.postMessage({ type: 'fleet-feedback:file', issue: issue });
|
|
847
|
+
this._status('Sent to the extension host.', 'ok');
|
|
848
|
+
var self = this;
|
|
849
|
+
root.setTimeout(function () { self.close(); }, 1200);
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
customElements.define('fleet-feedback', FleetFeedback);
|
|
853
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|