@sphoro/bugcatch-web-sdk 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +104 -6
  2. package/dist/bugcatch-call.es.js +587 -0
  3. package/dist/bugcatch-call.es.js.map +1 -0
  4. package/dist/bugcatch-call.umd.js +2 -0
  5. package/dist/bugcatch-call.umd.js.map +1 -0
  6. package/dist/bugcatch-core.es.js +3292 -0
  7. package/dist/bugcatch-core.es.js.map +1 -0
  8. package/dist/bugcatch-panel.es.js +2531 -0
  9. package/dist/bugcatch-panel.es.js.map +1 -0
  10. package/dist/bugcatch-panel.umd.js +1311 -0
  11. package/dist/bugcatch-panel.umd.js.map +1 -0
  12. package/dist/bugcatch.es.js +2 -5625
  13. package/dist/bugcatch.es.js.map +1 -1
  14. package/dist/bugcatch.standalone.umd.js +1619 -0
  15. package/dist/bugcatch.standalone.umd.js.map +1 -0
  16. package/dist/bugcatch.umd.js +195 -1318
  17. package/dist/bugcatch.umd.js.map +1 -1
  18. package/package.json +6 -6
  19. package/src/accent.js +38 -0
  20. package/src/announcements.js +147 -0
  21. package/src/busy.js +18 -0
  22. package/src/call.js +7 -0
  23. package/src/capture.js +9 -0
  24. package/src/chunks.js +56 -0
  25. package/src/gesture.js +33 -0
  26. package/src/icons.js +18 -0
  27. package/src/identity.js +13 -8
  28. package/src/incoming-call.js +25 -45
  29. package/src/index.d.ts +93 -2
  30. package/src/index.js +164 -42
  31. package/src/lazy.js +23 -0
  32. package/src/notice.js +68 -0
  33. package/src/panel/call.js +242 -0
  34. package/src/panel/compose.js +416 -0
  35. package/src/panel/composer.js +235 -0
  36. package/src/panel/icons.js +36 -0
  37. package/src/panel/index.js +265 -0
  38. package/src/panel/known.js +130 -0
  39. package/src/panel/messages.js +176 -0
  40. package/src/panel/notices.js +34 -0
  41. package/src/panel/picks.js +65 -0
  42. package/src/panel/refs.js +321 -0
  43. package/src/panel/render.js +132 -0
  44. package/src/panel/signin.js +228 -0
  45. package/src/panel/styles.js +1016 -0
  46. package/src/panel/thread.js +494 -0
  47. package/src/session.js +8 -3
  48. package/src/umd/call.js +6 -0
  49. package/src/umd/lazy.js +98 -0
  50. package/src/umd/panel.js +6 -0
  51. package/src/updates.js +97 -34
  52. package/src/widget.js +385 -3376
package/src/updates.js CHANGED
@@ -11,6 +11,9 @@
11
11
  // let anyone read anyone's reports. Holding this token is proof of having
12
12
  // filed them.
13
13
 
14
+ import { tooManyMessage } from './busy.js';
15
+ import { readableOn, INK, PAPER } from './accent.js';
16
+
14
17
  const TOKEN_KEY = 'bugcatch.watch';
15
18
  const BACKOFF_MS = [2000, 5000, 15000, 30000, 60000];
16
19
 
@@ -308,6 +311,7 @@ export async function sendMessage({ apiBase, bugId, body, attachments }) {
308
311
  const problem = await res.json();
309
312
  detail = Array.isArray(problem.message) ? problem.message.join(', ') : problem.message || '';
310
313
  } catch { /* non-JSON error */ }
314
+ if (res.status === 429) throw new Error(tooManyMessage(detail));
311
315
  throw new Error(detail || `[BugCatch] could not send that (HTTP ${res.status}).`);
312
316
  }
313
317
  return res.json();
@@ -384,7 +388,7 @@ export async function callKindsForProject({ apiBase, apiKey, projectId }) {
384
388
  * call could be placed — which is a success for the report and a failure only
385
389
  * for the call, and the caller is expected to tell them apart.
386
390
  */
387
- export async function reportByCall({ apiBase, apiKey, token, payload }) {
391
+ export async function reportByCall({ apiBase, apiKey, token, payload, watchToken }) {
388
392
  const res = await fetch(`${apiBase}/ingest/calls`, {
389
393
  method: 'POST',
390
394
  headers: {
@@ -396,7 +400,9 @@ export async function reportByCall({ apiBase, apiKey, token, payload }) {
396
400
  // is saying, an inch below the message, who they are signed in as.
397
401
  ...(token ? { 'X-BugCatch-Session': token } : {}),
398
402
  },
399
- body: JSON.stringify(payload),
403
+ // The watch token this browser holds, so the one filing hands back covers
404
+ // the reports it already could — see withHeldToken in index.js.
405
+ body: JSON.stringify(watchToken ? { ...payload, watchToken } : payload),
400
406
  });
401
407
 
402
408
  if (!res.ok) {
@@ -406,7 +412,8 @@ export async function reportByCall({ apiBase, apiKey, token, payload }) {
406
412
  detail = Array.isArray(problem.message) ? problem.message.join(', ') : problem.message || '';
407
413
  } catch { /* non-JSON error */ }
408
414
  const err = new Error(
409
- detail || `[BugCatch] the call could not be started (HTTP ${res.status}).`,
415
+ res.status === 429 ? tooManyMessage(detail)
416
+ : detail || `[BugCatch] the call could not be started (HTTP ${res.status}).`,
410
417
  );
411
418
  // Carried so a session the server has stopped recognising can be told from
412
419
  // every other refusal and forgotten, rather than pressing Call into the
@@ -652,9 +659,21 @@ const STATUS_COPY = {
652
659
  */
653
660
  const KIND_ACCENT = {
654
661
  status: null, // falls back to the host's configured accent
655
- severity: '#e8912a',
656
- assignment: '#4da3ff',
657
- reply: '#35b87f',
662
+ severity: '#f2a54a',
663
+ assignment: '#79a8ff',
664
+ reply: '#4fd19a',
665
+ };
666
+
667
+ // The cards are always dark — they float over pages of every colour, and a
668
+ // near-black card with a hairline reads on all of them. The colours are the
669
+ // product's dark palette.
670
+ const CARD = {
671
+ bg: '#111113',
672
+ fg: '#f3f2ef',
673
+ muted: '#b3b0aa',
674
+ faint: '#8f8c86',
675
+ ring: 'rgba(255,255,255,.09)',
676
+ brand: '#a59cff',
658
677
  };
659
678
 
660
679
  /**
@@ -730,7 +749,7 @@ function dockOffset() {
730
749
  * instead of a notification floating over the report it is about.
731
750
  */
732
751
  export function createUpdateNotifier({
733
- accent = '#4F46E5',
752
+ accent = null,
734
753
  position = 'bottom-right',
735
754
  onRead = null,
736
755
  onReadAll = null,
@@ -778,17 +797,23 @@ export function createUpdateNotifier({
778
797
  setTimeout(() => el.remove(), 250);
779
798
  }
780
799
 
800
+ // On a dark card the ink default is a white pill with ink words; a host's
801
+ // own colour keeps whichever words read on it.
802
+ const primaryBg = accent || PAPER;
803
+ const primaryFg = accent ? readableOn(accent) : INK;
804
+
781
805
  function button(label, primary) {
782
806
  const el = document.createElement('button');
783
807
  el.type = 'button';
784
808
  el.textContent = label;
785
809
  el.style.cssText = [
786
- 'padding:5px 11px',
787
- `border:${primary ? '0' : '1px solid rgba(236,238,242,.24)'}`,
788
- 'border-radius:6px',
789
- `background:${primary ? accent : 'transparent'}`,
790
- `color:${primary ? '#fff' : '#eceef2'}`,
791
- 'font:600 12px/1.4 inherit',
810
+ 'padding:6px 12px',
811
+ 'border:0',
812
+ `box-shadow:${primary ? 'none' : `inset 0 0 0 1px ${CARD.ring}`}`,
813
+ 'border-radius:999px',
814
+ `background:${primary ? primaryBg : 'rgba(255,255,255,.06)'}`,
815
+ `color:${primary ? primaryFg : CARD.fg}`,
816
+ 'font:560 12px/1.4 inherit',
792
817
  'cursor:pointer',
793
818
  'white-space:nowrap',
794
819
  ].join(';');
@@ -801,13 +826,12 @@ export function createUpdateNotifier({
801
826
  el.style.cssText = [
802
827
  'box-sizing:border-box',
803
828
  'width:100%',
804
- 'padding:12px 14px',
805
- 'border-radius:10px',
806
- 'background:#1b1e26',
807
- 'color:#eceef2',
829
+ 'padding:13px 14px',
830
+ 'border-radius:14px',
831
+ `background:${CARD.bg}`,
832
+ `color:${CARD.fg}`,
808
833
  'font:13px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
809
- 'box-shadow:0 12px 30px rgba(0,0,0,0.45)',
810
- `border-left:3px solid ${KIND_ACCENT[record?.kind] || accent}`,
834
+ `box-shadow:0 0 0 1px ${CARD.ring},0 20px 44px -12px rgba(0,0,0,.55),0 4px 12px rgba(0,0,0,.25)`,
811
835
  'opacity:0',
812
836
  `transform:translateY(${atTop ? -8 : 8}px)`,
813
837
  'transition:opacity .2s ease,transform .2s ease',
@@ -815,25 +839,33 @@ export function createUpdateNotifier({
815
839
  ].join(';');
816
840
 
817
841
  const title = document.createElement('div');
818
- title.style.cssText = 'font-weight:600;margin-bottom:2px';
819
- title.textContent = updateHeadline(record);
842
+ title.style.cssText = 'display:flex;align-items:baseline;gap:8px;font-weight:600;margin-bottom:2px';
843
+ // The kind's colour, as a dot before the headline rather than a stripe
844
+ // down the card: a fix and a severity change are still told apart.
845
+ const dot = document.createElement('span');
846
+ dot.setAttribute('aria-hidden', 'true');
847
+ dot.style.cssText = `flex:none;width:7px;height:7px;border-radius:50%;transform:translateY(-1px);background:${
848
+ KIND_ACCENT[record?.kind] || accent || CARD.brand}`;
849
+ const headline = document.createElement('span');
850
+ headline.textContent = updateHeadline(record);
851
+ title.append(dot, headline);
820
852
 
821
853
  const body = document.createElement('div');
822
854
  // A reply is the one kind whose detail is worth more room than a title, and
823
855
  // it is the only text here the team wrote rather than the reporter.
824
856
  body.style.cssText = record?.kind === 'reply'
825
- ? 'color:#a4abba;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden'
826
- : 'color:#a4abba';
857
+ ? `color:${CARD.muted};padding-left:15px;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden`
858
+ : `color:${CARD.muted};padding-left:15px`;
827
859
  // textContent, not innerHTML: this is text a person typed, being written
828
860
  // back into somebody else's page.
829
861
  body.textContent = updateDetail(record);
830
862
 
831
863
  const actions = document.createElement('div');
832
864
  actions.style.cssText =
833
- 'display:flex;align-items:center;gap:8px;margin-top:10px;flex-wrap:wrap';
865
+ 'display:flex;align-items:center;gap:8px;margin-top:10px;padding-left:15px;flex-wrap:wrap';
834
866
 
835
867
  const when = document.createElement('span');
836
- when.style.cssText = 'color:#7b8394;font-size:11px;margin-right:auto';
868
+ when.style.cssText = `color:${CARD.faint};font-size:11px;margin-right:auto`;
837
869
  when.textContent = agoLabel(record?.at);
838
870
 
839
871
  const read = button('Mark as read', false);
@@ -865,12 +897,12 @@ export function createUpdateNotifier({
865
897
  'display:flex',
866
898
  'align-items:center',
867
899
  'gap:8px',
868
- 'padding:8px 12px',
869
- 'border-radius:10px',
870
- 'background:#1b1e26',
871
- 'color:#a4abba',
900
+ 'padding:8px 10px 8px 14px',
901
+ 'border-radius:999px',
902
+ `background:${CARD.bg}`,
903
+ `color:${CARD.muted}`,
872
904
  'font:12px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
873
- 'box-shadow:0 12px 30px rgba(0,0,0,0.45)',
905
+ `box-shadow:0 0 0 1px ${CARD.ring},0 12px 30px -10px rgba(0,0,0,.5)`,
874
906
  'pointer-events:auto',
875
907
  ].join(';');
876
908
 
@@ -1020,12 +1052,18 @@ export async function matchKnownIssues({ apiBase, apiKey, projectId, q, sessionT
1020
1052
  export async function joinKnownIssue({
1021
1053
  apiBase, apiKey, projectId, bugId, reporterEmail, reporterName, metadata, logs, sessionToken,
1022
1054
  }) {
1055
+ // The token this browser already holds goes back with the join, so the one
1056
+ // that comes back still covers the reports it filed as well as this issue.
1057
+ const held = readToken();
1023
1058
  const res = await fetch(
1024
1059
  `${apiBase}/ingest/known-issues/${encodeURIComponent(bugId)}/join`,
1025
1060
  {
1026
1061
  method: 'POST',
1027
1062
  headers: knownIssueHeaders(apiKey, sessionToken),
1028
- body: JSON.stringify({ projectId, reporterEmail, reporterName, metadata, logs }),
1063
+ body: JSON.stringify({
1064
+ projectId, reporterEmail, reporterName, metadata, logs,
1065
+ ...(held ? { watchToken: held } : {}),
1066
+ }),
1029
1067
  },
1030
1068
  );
1031
1069
  if (!res.ok) throw new Error(await readJoinError(res));
@@ -1043,12 +1081,37 @@ function knownIssueHeaders(apiKey, sessionToken) {
1043
1081
  }
1044
1082
 
1045
1083
  async function readJoinError(res) {
1084
+ let message = '';
1046
1085
  try {
1047
1086
  const body = await res.json();
1048
- const message = Array.isArray(body.message) ? body.message.join(', ') : body.message;
1049
- if (message) return message;
1087
+ message = Array.isArray(body.message) ? body.message.join(', ') : body.message || '';
1050
1088
  } catch { /* non-JSON error */ }
1051
- return `Could not add you to this issue (HTTP ${res.status})`;
1089
+ if (res.status === 429) return tooManyMessage(message);
1090
+ return message || `Could not add you to this issue (HTTP ${res.status})`;
1091
+ }
1092
+
1093
+ /**
1094
+ * Rate how a settled report was handled, out of five, with a sentence if they
1095
+ * want one. Rating again replaces the earlier score.
1096
+ *
1097
+ * Same credential as everything else here: the watch token, which is proof of
1098
+ * having filed the report. `?projectId=` goes with it so a token that reaches
1099
+ * more than one project is narrowed to the one this widget is on.
1100
+ */
1101
+ export async function rateReport({ apiBase, bugId, score, comment }) {
1102
+ const token = readToken();
1103
+ if (!token) throw new Error('This browser cannot access that report.');
1104
+ const res = await fetch(`${apiBase}/ingest/watch/reports/${encodeURIComponent(bugId)}/rating${projectQuery()}`, {
1105
+ method: 'POST',
1106
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
1107
+ body: JSON.stringify({ score, ...(comment ? { comment } : {}) }),
1108
+ });
1109
+ const data = await res.json().catch(() => ({}));
1110
+ if (!res.ok) {
1111
+ const message = Array.isArray(data.message) ? data.message.join(', ') : data.message;
1112
+ throw new Error(message || 'Could not send your rating. Please try again.');
1113
+ }
1114
+ return data;
1052
1115
  }
1053
1116
 
1054
1117
  export async function verifyFix({ apiBase, bugId, ...feedback }) {