@sphoro/bugcatch-web-sdk 0.18.0 → 0.20.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,87 @@
1
+ // "How did we do?" — five stars and, after them, an optional note. Drawn under
2
+ // a settled report's conversation (thread.js) and above the form on the first
3
+ // screen (rate-due.js), so both ask the same question the same way.
4
+
5
+ import * as ICONS from './icons.js';
6
+ import { escapeHtml } from './render.js';
7
+
8
+ /** What a drawn rating box was drawn from, so a refresh can tell if it moved. */
9
+ export function ratingKey(rating) {
10
+ return JSON.stringify(rating ? [rating.score, rating.current, rating.comment || null] : null);
11
+ }
12
+
13
+ /**
14
+ * Draw the stars into `box` for `report`, with the reader's `rating` so far.
15
+ *
16
+ * One tap sends the score; a note is offered after it rather than before,
17
+ * because a form that asks for a sentence first is a form people close, and
18
+ * the score is the part that counts. `onRated` hears each saved rating and
19
+ * `onError` each refusal, for a caller that holds something until it lands.
20
+ */
21
+ export function drawRating(box, {
22
+ client, report, rating, justRated = false, question = null, onRated = null, onError = null,
23
+ }) {
24
+ box.dataset.key = ratingKey(rating);
25
+ // A rating given for an earlier fix stands, but it is not this fix's: the
26
+ // stars start empty and the sentence says what the old score was.
27
+ const shown = rating?.current ? rating.score : 0;
28
+ const asked = !rating
29
+ ? (question || 'How did we do?')
30
+ : !rating.current
31
+ ? `You rated an earlier fix ${rating.score} out of 5. How did this one go?`
32
+ : justRated
33
+ ? 'Thanks — the team will see that.'
34
+ : `You rated this ${rating.score} out of 5.`;
35
+ const askNote = justRated && rating?.current && !rating.comment;
36
+ box.innerHTML = `
37
+ <p class="fr-q">${escapeHtml(asked)}</p>
38
+ <div class="stars" role="group" aria-label="Rate how this was handled, 1 to 5">${[1, 2, 3, 4, 5].map((n) => `
39
+ <button type="button" data-score="${n}" aria-pressed="${n === shown}" aria-label="${n} out of 5${n === 1 ? ', poor' : n === 5 ? ', great' : ''}" class="${n <= shown ? 'on' : ''}">${ICONS.star}</button>`).join('')}
40
+ </div>
41
+ ${rating?.current && rating.comment ? `<p class="fr-said">“${escapeHtml(rating.comment)}”</p>` : ''}
42
+ ${askNote ? `
43
+ <div class="fr-note">
44
+ <textarea maxlength="2000" aria-label="Add a note to your rating" placeholder="${rating.score <= 2 ? 'What went wrong? (optional)' : 'Anything to add? (optional)'}"></textarea>
45
+ <button type="button" data-act="rating-note">Add note</button>
46
+ </div>` : ''}
47
+ ${rating ? '' : '<p class="fr-hint">1 is poor, 5 is great.</p>'}
48
+ <p class="feedback-status" role="status"></p>`;
49
+
50
+ const stars = [...box.querySelectorAll('[data-score]')];
51
+ const paint = (upTo) => stars.forEach((b) => b.classList.toggle('on', Number(b.dataset.score) <= upTo));
52
+ const lock = (on) => box.querySelectorAll('button, textarea').forEach((el) => { el.disabled = on; });
53
+ const status = box.querySelector('.feedback-status');
54
+
55
+ async function send(score, comment) {
56
+ lock(true);
57
+ status.textContent = '';
58
+ try {
59
+ const saved = await client.rate(report.id, { score, comment });
60
+ drawRating(box, {
61
+ client, report, rating: saved, justRated: true, question, onRated, onError,
62
+ });
63
+ box.querySelector('.fr-note textarea')?.focus();
64
+ onRated?.(saved);
65
+ } catch (err) {
66
+ status.textContent = err.message;
67
+ lock(false);
68
+ paint(shown);
69
+ onError?.(err);
70
+ }
71
+ }
72
+
73
+ for (const btn of stars) {
74
+ const n = Number(btn.dataset.score);
75
+ btn.addEventListener('mouseenter', () => paint(n));
76
+ btn.addEventListener('focus', () => paint(n));
77
+ btn.addEventListener('blur', () => paint(shown));
78
+ btn.addEventListener('click', () => send(n));
79
+ }
80
+ box.querySelector('.stars').addEventListener('mouseleave', () => paint(shown));
81
+
82
+ box.querySelector('[data-act="rating-note"]')?.addEventListener('click', () => {
83
+ const note = box.querySelector('.fr-note textarea').value.trim();
84
+ if (!note) { box.querySelector('.fr-note')?.remove(); return; }
85
+ send(rating.score, note);
86
+ });
87
+ }
package/src/panel/refs.js CHANGED
@@ -137,7 +137,7 @@ export function mountReferences(ctx) {
137
137
  const many = kinds.length > 6;
138
138
  refsBox.innerHTML = `
139
139
  <div class="ask-head">
140
- <span class="avatar" aria-hidden="true">${ICONS.sparkle}</span>
140
+ <span class="avatar orb" aria-hidden="true"></span>
141
141
  <div>
142
142
  <strong>${escapeHtml(head[0])}</strong>
143
143
  <p class="why">${escapeHtml(head[1])}</p>
@@ -6,18 +6,26 @@
6
6
  import * as ICONS from './icons.js';
7
7
 
8
8
  /**
9
- * The panel's header, on every screen. The assistant's avatar carries the
9
+ * The panel's header, on every screen. The assistant's orb carries the
10
10
  * identity; `back` adds the way out of a report to the list of them.
11
+ *
12
+ * `theme` is the name of the theme in force ("Light", "Dark", "System
13
+ * default"); given one, the header carries the button that moves to the next.
14
+ * The button holds all three glyphs and the stylesheet shows the one matching
15
+ * the host element's class, so it is right on every screen without anyone
16
+ * redrawing it. Its press is handled once, on the scrim (index.js).
11
17
  */
12
- export function headHtml({ title, sub, id = '', back = false }) {
18
+ export function headHtml({ title, sub, id = '', back = false, theme = '' }) {
13
19
  return `
14
20
  <div class="head">
15
21
  ${back ? `<button class="icon-btn back" type="button" data-act="back" aria-label="Back to your reports">${ICONS.back}</button>` : ''}
16
- <div class="mark" aria-hidden="true">${ICONS.sparkle}</div>
22
+ <span class="orb mark" aria-hidden="true"></span>
17
23
  <div class="titles">
18
24
  <h2${id ? ` id="${id}"` : ''}>${escapeHtml(title)}</h2>
19
25
  <p>${escapeHtml(sub)}</p>
20
26
  </div>
27
+ ${theme ? `<button class="icon-btn theme" type="button" data-act="theme" title="Theme: ${escapeHtml(theme)}"
28
+ aria-label="Theme: ${escapeHtml(theme)}. Change theme"><span class="m-light">${ICONS.sun}</span><span class="m-dark">${ICONS.moon}</span><span class="m-auto">${ICONS.auto}</span></button>` : ''}
21
29
  <button class="icon-btn" type="button" data-act="close" aria-label="Close">${ICONS.close}</button>
22
30
  </div>`;
23
31
  }
@@ -37,7 +37,7 @@ export function mountSignIn(ctx) {
37
37
  // The call offer is the same errand as the form, so it goes with it — and
38
38
  // comes back only if the API had already said this project can take one.
39
39
  if (shut) callBtn.hidden = true;
40
- else if (ctx.callOffered()) callBtn.hidden = false;
40
+ else if (ctx.callOffered() && !ctx.mustRate()) callBtn.hidden = false;
41
41
  }
42
42
 
43
43
  function drawWhoami() {