@sphoro/bugcatch-web-sdk 0.20.0 → 0.21.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.
- package/README.md +39 -1
- package/dist/bugcatch-call.umd.js +1 -1
- package/dist/bugcatch-core.es.js +652 -497
- package/dist/bugcatch-core.es.js.map +1 -1
- package/dist/bugcatch-panel.es.js +1140 -938
- package/dist/bugcatch-panel.es.js.map +1 -1
- package/dist/bugcatch-panel.umd.js +237 -145
- package/dist/bugcatch-panel.umd.js.map +1 -1
- package/dist/bugcatch.standalone.umd.js +243 -151
- package/dist/bugcatch.standalone.umd.js.map +1 -1
- package/dist/bugcatch.umd.js +8 -8
- package/dist/bugcatch.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/feedback.js +94 -0
- package/src/identity.js +1 -1
- package/src/index.d.ts +56 -6
- package/src/index.js +111 -7
- package/src/panel/compose.js +1 -1
- package/src/panel/feedback.js +205 -0
- package/src/panel/icons.js +6 -0
- package/src/panel/index.js +83 -13
- package/src/panel/styles.js +47 -0
- package/src/widget.js +14 -1
package/src/panel/index.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// One dialog doing several jobs, one file each:
|
|
9
9
|
//
|
|
10
10
|
// compose.js the report form: drafts, severity, attachments, recording, Send
|
|
11
|
+
// feedback.js the other form: "Give feedback", for what is not a bug
|
|
11
12
|
// rate-due.js "how did we do?" about a settled report, above the form
|
|
12
13
|
// refs.js "which record is this about", asked at Send
|
|
13
14
|
// notices.js what the project's team is telling visitors, at the top
|
|
@@ -39,6 +40,16 @@ import { mountCallOffer } from './call.js';
|
|
|
39
40
|
import { mountSignIn } from './signin.js';
|
|
40
41
|
import { mountThread } from './thread.js';
|
|
41
42
|
import { mountRateDue } from './rate-due.js';
|
|
43
|
+
import { feedbackHtml, mountFeedback } from './feedback.js';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What the header says on each of the two forms. The header names the errand;
|
|
47
|
+
* the switch under it is how to change errands.
|
|
48
|
+
*/
|
|
49
|
+
const MODES = {
|
|
50
|
+
report: { title: 'Report a bug', sub: 'We attach the page details for you.' },
|
|
51
|
+
feedback: { title: 'Give feedback', sub: 'The team reads every note.' },
|
|
52
|
+
};
|
|
42
53
|
|
|
43
54
|
/**
|
|
44
55
|
* Open the panel over the page. `preloadCall` fetches the call code ahead of a
|
|
@@ -46,11 +57,17 @@ import { mountRateDue } from './rate-due.js';
|
|
|
46
57
|
* to load code of its own.
|
|
47
58
|
*/
|
|
48
59
|
export function openPanel({
|
|
49
|
-
root, host, dock, client, onClose, preloadCall = () => {}, themeControls = null,
|
|
60
|
+
root, host, dock, client, onClose, preloadCall = () => {}, themeControls = null, mode: askedMode = 'report',
|
|
50
61
|
}) {
|
|
51
62
|
addStyles(root);
|
|
52
63
|
const cfg = client.config;
|
|
53
64
|
const themeLabel = () => themeControls?.label() || '';
|
|
65
|
+
// Drawn straight away rather than once the API has answered, because nearly
|
|
66
|
+
// every project has it on: a switch that arrived a beat after the form would
|
|
67
|
+
// push the form down under the reader's cursor. The rare project that has it
|
|
68
|
+
// off loses it again when the answer comes (below).
|
|
69
|
+
const offerFeedback = cfg.feedback !== false;
|
|
70
|
+
let mode = offerFeedback && askedMode === 'feedback' ? 'feedback' : 'report';
|
|
54
71
|
|
|
55
72
|
const scrim = document.createElement('div');
|
|
56
73
|
// The panel opens out of whichever corner the dock is in, clearing it by the
|
|
@@ -59,40 +76,47 @@ export function openPanel({
|
|
|
59
76
|
+ `${dock.classList.contains('at-top') ? 'at-top' : 'at-bottom'}`;
|
|
60
77
|
scrim.style.setProperty('--bc-dock-h', `${Math.round(dock.getBoundingClientRect().height)}px`);
|
|
61
78
|
scrim.innerHTML = `
|
|
62
|
-
<div class="panel" role="dialog" aria-modal="true" aria-labelledby="bc-h">
|
|
79
|
+
<div class="panel" role="dialog" aria-modal="true" aria-labelledby="bc-h" data-mode="${mode}">
|
|
63
80
|
${headHtml({
|
|
64
81
|
id: 'bc-h',
|
|
65
82
|
// Not "Tell us what went wrong": that is the first field's own label,
|
|
66
83
|
// a few pixels below. The header names the errand the launcher named.
|
|
67
|
-
title:
|
|
68
|
-
sub:
|
|
84
|
+
title: MODES[mode].title,
|
|
85
|
+
sub: MODES[mode].sub,
|
|
69
86
|
theme: themeLabel(),
|
|
70
87
|
})}
|
|
88
|
+
${offerFeedback ? `
|
|
89
|
+
<div class="modes" role="group" aria-label="What would you like to do?">
|
|
90
|
+
<button type="button" data-mode="report" aria-pressed="${mode === 'report'}">${ICONS.bug}Report a bug</button>
|
|
91
|
+
<button type="button" data-mode="feedback" aria-pressed="${mode === 'feedback'}">${ICONS.feedback}Give feedback</button>
|
|
92
|
+
</div>` : ''}
|
|
71
93
|
<div class="body">
|
|
72
94
|
<div class="slot" role="alert"></div>
|
|
73
95
|
<!-- A settled report still waiting for its rating. Drawn only when the
|
|
74
96
|
project asks for one here; see rate-due.js. -->
|
|
75
|
-
<div class="rate-due fix-rating" tabindex="-1" hidden></div>
|
|
97
|
+
<div class="rate-due fix-rating bug-only" tabindex="-1" hidden></div>
|
|
76
98
|
<!-- What the project's team is telling visitors. Drawn only when it
|
|
77
99
|
has published something. -->
|
|
78
100
|
<div class="notices" hidden></div>
|
|
79
|
-
<div class="whoami" hidden></div>
|
|
101
|
+
<div class="whoami bug-only" hidden></div>
|
|
80
102
|
<!-- The questions the assistant can answer from their account and
|
|
81
103
|
close by itself. Drawn only when the project offers any. -->
|
|
82
|
-
<div class="picks" hidden></div>
|
|
83
|
-
<div class="known" hidden></div>
|
|
104
|
+
<div class="picks bug-only" hidden></div>
|
|
105
|
+
<div class="known bug-only" hidden></div>
|
|
84
106
|
${composeHtml(cfg)}
|
|
107
|
+
${offerFeedback ? feedbackHtml() : ''}
|
|
85
108
|
</div>
|
|
86
109
|
<div class="foot">
|
|
87
|
-
<span class="note" aria-live="polite"></span>
|
|
110
|
+
<span class="note bug-only" aria-live="polite"></span>
|
|
88
111
|
${client.hasReports() ? '<button class="quiet" type="button" data-act="history">Your reports</button>' : ''}
|
|
89
112
|
<span class="spacer"></span>
|
|
90
113
|
<!-- Hidden until the API confirms this project can take a call, which
|
|
91
114
|
most cannot. Drawn here rather than as a second launcher because it
|
|
92
115
|
is the same errand as Send report — the difference is only whether
|
|
93
116
|
the reporter would rather say it than type it. -->
|
|
94
|
-
<button class="ghost" type="button" data-act="call" hidden>${ICONS.phone}Talk to the team</button>
|
|
95
|
-
<button class="primary" type="button" data-act="send">Send report ${ICONS.send}</button>
|
|
117
|
+
<button class="ghost bug-only" type="button" data-act="call" hidden>${ICONS.phone}Talk to the team</button>
|
|
118
|
+
<button class="primary bug-only" type="button" data-act="send">Send report ${ICONS.send}</button>
|
|
119
|
+
${offerFeedback ? `<button class="primary fb-only" type="button" data-act="send-feedback">Send feedback ${ICONS.send}</button>` : ''}
|
|
96
120
|
</div>
|
|
97
121
|
<div class="powered">Powered by
|
|
98
122
|
<a class="brand-credit" href="https://bugcatch.sphoro.com" target="_blank" rel="noopener noreferrer" aria-label="BugCatch">${BRAND}</a>
|
|
@@ -163,6 +187,8 @@ export function openPanel({
|
|
|
163
187
|
recheckSignIn: null,
|
|
164
188
|
// rate-due.js
|
|
165
189
|
mustRate: () => false,
|
|
190
|
+
// feedback.js
|
|
191
|
+
focusFeedback: () => {},
|
|
166
192
|
// thread.js
|
|
167
193
|
succeed: null,
|
|
168
194
|
refreshThread: null,
|
|
@@ -180,6 +206,49 @@ export function openPanel({
|
|
|
180
206
|
mountSignIn(ctx);
|
|
181
207
|
mountThread(ctx);
|
|
182
208
|
mountRateDue(ctx);
|
|
209
|
+
if (offerFeedback) mountFeedback(ctx);
|
|
210
|
+
|
|
211
|
+
/* ---------- report a bug, or give feedback ---------- */
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Move between the two forms. Only while the forms are on screen: history,
|
|
215
|
+
* a report's conversation and the call screens replace the panel's markup,
|
|
216
|
+
* and there is no form there to switch to. Nothing typed into either form is
|
|
217
|
+
* lost by switching — both stay in the page, one of them hidden.
|
|
218
|
+
*/
|
|
219
|
+
function setMode(next, { focus = true } = {}) {
|
|
220
|
+
const want = next === 'feedback' && $('.modes') ? 'feedback' : 'report';
|
|
221
|
+
const panel = scrim.querySelector('.panel');
|
|
222
|
+
if (ctx.closed || ctx.sending || !panel || !scrim.querySelector('.modes')) return;
|
|
223
|
+
if (want === 'feedback' && ctx.isRecording()) return;
|
|
224
|
+
mode = want;
|
|
225
|
+
panel.dataset.mode = want;
|
|
226
|
+
panel.querySelector('#bc-h').textContent = MODES[want].title;
|
|
227
|
+
panel.querySelector('.head .titles p').textContent = MODES[want].sub;
|
|
228
|
+
scrim.querySelectorAll('.modes [data-mode]').forEach((b) => {
|
|
229
|
+
b.setAttribute('aria-pressed', String(b.dataset.mode === want));
|
|
230
|
+
});
|
|
231
|
+
say('');
|
|
232
|
+
if (focus) (want === 'feedback' ? ctx.focusFeedback() : ctx.titleInput.focus());
|
|
233
|
+
}
|
|
234
|
+
scrim.querySelectorAll('.modes [data-mode]').forEach((btn) => {
|
|
235
|
+
btn.addEventListener('click', () => setMode(btn.dataset.mode));
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// The project's own answer. Off — or an API too old to say — takes the
|
|
239
|
+
// switch away, and with it a feedback form the server would refuse.
|
|
240
|
+
if (offerFeedback) {
|
|
241
|
+
client.feedbackEnabled().then((on) => {
|
|
242
|
+
// Mid-send, the form stays for this open: the server's refusal says why,
|
|
243
|
+
// which beats a form vanishing from under the button just pressed.
|
|
244
|
+
if (on || ctx.closed || ctx.sending) return;
|
|
245
|
+
const wasFeedback = mode === 'feedback';
|
|
246
|
+
setMode('report', { focus: wasFeedback });
|
|
247
|
+
scrim.querySelector('.modes')?.remove();
|
|
248
|
+
scrim.querySelector('.feedback')?.remove();
|
|
249
|
+
scrim.querySelector('[data-act="send-feedback"]')?.remove();
|
|
250
|
+
});
|
|
251
|
+
}
|
|
183
252
|
|
|
184
253
|
/* ---------- lifecycle ---------- */
|
|
185
254
|
|
|
@@ -251,9 +320,10 @@ export function openPanel({
|
|
|
251
320
|
|
|
252
321
|
root.appendChild(scrim);
|
|
253
322
|
ctx.refreshSendState();
|
|
254
|
-
ctx.
|
|
323
|
+
if (mode === 'feedback') ctx.focusFeedback();
|
|
324
|
+
else ctx.titleInput.focus();
|
|
255
325
|
|
|
256
|
-
return { destroy, close };
|
|
326
|
+
return { destroy, close, setMode };
|
|
257
327
|
}
|
|
258
328
|
|
|
259
329
|
/**
|
package/src/panel/styles.js
CHANGED
|
@@ -306,6 +306,53 @@ textarea { min-height: 76px; resize: vertical; }
|
|
|
306
306
|
.seg button[aria-pressed=true]::before { opacity: 1; }
|
|
307
307
|
.seg button:focus-visible { outline-offset: -2px; }
|
|
308
308
|
|
|
309
|
+
/* ---------- report a bug, or give feedback ----------
|
|
310
|
+
The same stone track as the severity control, two wide segments, under the
|
|
311
|
+
header so it stays put while the form scrolls. Each form's own parts carry
|
|
312
|
+
bug-only or fb-only; the other screens (history, a conversation, a call)
|
|
313
|
+
carry neither, so a mode left on the panel cannot hide anything of theirs. */
|
|
314
|
+
.modes {
|
|
315
|
+
flex: none; display: grid; grid-template-columns: 1fr 1fr; gap: 2px;
|
|
316
|
+
margin: 12px var(--bc-pad) 0; padding: 3px; background: var(--bc-stone); border-radius: 999px;
|
|
317
|
+
}
|
|
318
|
+
.modes button {
|
|
319
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-width: 0;
|
|
320
|
+
height: 32px; padding: 0 8px; font: inherit; font-size: var(--bc-fs-sm); font-weight: 520; line-height: 1; white-space: nowrap;
|
|
321
|
+
color: var(--bc-muted); background: none; border: none; border-radius: 999px;
|
|
322
|
+
cursor: pointer; transition: background 180ms var(--bc-ease), color 180ms, box-shadow 180ms;
|
|
323
|
+
}
|
|
324
|
+
.modes button svg { font-size: 15px; flex: none; }
|
|
325
|
+
.modes button:hover { color: var(--bc-fg); }
|
|
326
|
+
.modes button[aria-pressed=true] {
|
|
327
|
+
background: var(--bc-raised); color: var(--bc-fg); font-weight: 600;
|
|
328
|
+
box-shadow: 0 1px 2px rgba(20,18,14,.10), 0 0 0 .5px rgba(20,18,14,.08);
|
|
329
|
+
}
|
|
330
|
+
.modes button:focus-visible { outline-offset: -2px; }
|
|
331
|
+
.panel[data-mode="feedback"] .bug-only,
|
|
332
|
+
.panel:not([data-mode="feedback"]) .fb-only { display: none !important; }
|
|
333
|
+
|
|
334
|
+
/* ---------- feedback ---------- */
|
|
335
|
+
.feedback { animation: bc-fade-in 200ms ease both; }
|
|
336
|
+
.feedback textarea { min-height: 96px; }
|
|
337
|
+
.feedback .kinds { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
338
|
+
.feedback .kinds .tool[aria-pressed=true] { background: var(--bc-accent); border-color: var(--bc-accent); color: var(--bc-on-accent); }
|
|
339
|
+
.feedback .kinds .tool[aria-pressed=true] svg { color: currentColor; }
|
|
340
|
+
.faces { display: flex; gap: 6px; }
|
|
341
|
+
.face {
|
|
342
|
+
flex: 1 1 0; display: grid; place-items: center; height: 42px; padding: 0;
|
|
343
|
+
font: inherit; font-size: 24px; color: var(--bc-muted); background: var(--bc-stone);
|
|
344
|
+
border: 1px solid transparent; border-radius: var(--bc-r-md); cursor: pointer;
|
|
345
|
+
transition: background 160ms, color 160ms, transform 160ms var(--bc-ease);
|
|
346
|
+
}
|
|
347
|
+
.face:hover { color: var(--bc-fg); background: color-mix(in srgb, var(--bc-fg) 8%, var(--bc-stone)); }
|
|
348
|
+
.face:active { transform: scale(.94); }
|
|
349
|
+
.face[aria-pressed=true] { color: var(--bc-on-accent); background: var(--bc-accent); }
|
|
350
|
+
.face:focus-visible { outline-offset: 2px; }
|
|
351
|
+
.face-word { font-weight: 560; color: var(--bc-muted); }
|
|
352
|
+
.fb-as { margin: 0; font-size: var(--bc-fs-xs); line-height: 1.45; color: var(--bc-muted); }
|
|
353
|
+
.fb-as strong { color: var(--bc-fg); font-weight: 600; word-break: break-all; }
|
|
354
|
+
.fb-who:empty { display: none; }
|
|
355
|
+
|
|
309
356
|
/* ---------- attachments ----------
|
|
310
357
|
Three ring pills in a row: optional, so they take one line, not a band of
|
|
311
358
|
dashed drop zones. */
|
package/src/widget.js
CHANGED
|
@@ -423,7 +423,13 @@ export function mountWidget(client) {
|
|
|
423
423
|
open = openPanelWhenLoaded();
|
|
424
424
|
});
|
|
425
425
|
|
|
426
|
+
// Which form the next panel opens on: BugCatch.open({ mode }) sets it just
|
|
427
|
+
// before pressing the launcher, and a person pressing it gets the report form.
|
|
428
|
+
let nextMode = 'report';
|
|
429
|
+
|
|
426
430
|
function openPanelWhenLoaded() {
|
|
431
|
+
let mode = nextMode;
|
|
432
|
+
nextMode = 'report';
|
|
427
433
|
hideLoadFailure();
|
|
428
434
|
// The panel says it again, at the top, with the rest of what is live.
|
|
429
435
|
hideNotice();
|
|
@@ -437,6 +443,8 @@ export function mountWidget(client) {
|
|
|
437
443
|
const pending = {
|
|
438
444
|
close: () => (panel ? panel.close() : cancel()),
|
|
439
445
|
destroy: () => (panel ? panel.destroy() : cancel()),
|
|
446
|
+
// Asked again while it is on its way, or once it is open.
|
|
447
|
+
setMode: (m) => { if (panel) panel.setMode?.(m); else mode = m; },
|
|
440
448
|
};
|
|
441
449
|
|
|
442
450
|
launcher.setAttribute('aria-busy', 'true');
|
|
@@ -452,6 +460,7 @@ export function mountWidget(client) {
|
|
|
452
460
|
client,
|
|
453
461
|
preloadCall,
|
|
454
462
|
themeControls,
|
|
463
|
+
mode,
|
|
455
464
|
onClose: () => {
|
|
456
465
|
open = null;
|
|
457
466
|
dock.classList.remove('over');
|
|
@@ -558,7 +567,11 @@ export function mountWidget(client) {
|
|
|
558
567
|
// Integration hook: BugCatch.open() / close() / isOpen() drive the same
|
|
559
568
|
// launcher a person would press, so there is one way in to the panel.
|
|
560
569
|
client.widgetControls = {
|
|
561
|
-
open: () => {
|
|
570
|
+
open: (mode) => {
|
|
571
|
+
if (open) { if (mode) open.setMode(mode); return; }
|
|
572
|
+
nextMode = mode || 'report';
|
|
573
|
+
launcher.click();
|
|
574
|
+
},
|
|
562
575
|
close: () => { open?.close(); },
|
|
563
576
|
isOpen: () => Boolean(open),
|
|
564
577
|
};
|