feedbackkit-web 1.0.37 → 1.0.38
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 +24 -0
- package/dist/feedbackkit.iife.js +45 -11
- package/dist/feedbackkit.iife.js.map +1 -1
- package/dist/feedbackkit.js +463 -58
- package/dist/feedbackkit.js.map +1 -1
- package/dist/types/fixes.d.ts +66 -0
- package/dist/types/global.d.ts +7 -0
- package/dist/types/index.d.ts +38 -1
- package/dist/types/submit.d.ts +13 -3
- package/dist/types/types.d.ts +43 -0
- package/dist/types/ui/fixCard.d.ts +32 -0
- package/dist/types/ui/styles.d.ts +1 -1
- package/dist/types/ui/widget.d.ts +30 -1
- package/package.json +1 -1
package/dist/feedbackkit.js
CHANGED
|
@@ -307,9 +307,9 @@ class LogBuffer {
|
|
|
307
307
|
this.entries = [];
|
|
308
308
|
}
|
|
309
309
|
push(level, message) {
|
|
310
|
-
let
|
|
311
|
-
if (
|
|
312
|
-
this.entries.push({ level, message:
|
|
310
|
+
let text2 = redactMessage(message);
|
|
311
|
+
if (text2.length > MAX_MESSAGE_LENGTH) text2 = `${text2.slice(0, MAX_MESSAGE_LENGTH)}… [truncated]`;
|
|
312
|
+
this.entries.push({ level, message: text2, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
313
313
|
if (this.entries.length > this.maxEntries) this.entries.splice(0, this.entries.length - this.maxEntries);
|
|
314
314
|
}
|
|
315
315
|
snapshot() {
|
|
@@ -433,7 +433,7 @@ async function blobToBase64(blob) {
|
|
|
433
433
|
}
|
|
434
434
|
return btoa(binary);
|
|
435
435
|
}
|
|
436
|
-
async function encodePayload(report, projectKey) {
|
|
436
|
+
async function encodePayload(report, projectKey, identity = {}) {
|
|
437
437
|
const payload = {
|
|
438
438
|
project_key: projectKey,
|
|
439
439
|
id: report.id,
|
|
@@ -465,13 +465,15 @@ async function encodePayload(report, projectKey) {
|
|
|
465
465
|
}));
|
|
466
466
|
}
|
|
467
467
|
if (report.logs.length > 0) payload.logs = report.logs;
|
|
468
|
+
if (identity.reporterId) payload.reporter_id = identity.reporterId;
|
|
469
|
+
if (identity.user) payload.reporter = identity.user;
|
|
468
470
|
return payload;
|
|
469
471
|
}
|
|
470
|
-
async function submitReport(report, configuration2) {
|
|
472
|
+
async function submitReport(report, configuration2, identity = {}) {
|
|
471
473
|
if (!configuration2.projectKey) {
|
|
472
474
|
throw new FeedbackSubmissionError("FeedbackKit is not configured with a project key.", "not_configured");
|
|
473
475
|
}
|
|
474
|
-
const body = JSON.stringify(await encodePayload(report, configuration2.projectKey));
|
|
476
|
+
const body = JSON.stringify(await encodePayload(report, configuration2.projectKey, identity));
|
|
475
477
|
let response;
|
|
476
478
|
try {
|
|
477
479
|
response = await fetch(configuration2.endpoint ?? DEFAULT_ENDPOINT, {
|
|
@@ -510,6 +512,246 @@ async function fetchProducts(configuration2) {
|
|
|
510
512
|
isDefault: p.is_default ?? false
|
|
511
513
|
}));
|
|
512
514
|
}
|
|
515
|
+
function compareBuilds(a, b) {
|
|
516
|
+
if (a == null || b == null || a === "" || b === "") return null;
|
|
517
|
+
if (a === b) return 0;
|
|
518
|
+
const numeric = /^[0-9]+(\.[0-9]+)*$/;
|
|
519
|
+
if (!numeric.test(a) || !numeric.test(b)) return null;
|
|
520
|
+
const pa = a.split(".");
|
|
521
|
+
const pb = b.split(".");
|
|
522
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
523
|
+
const na = BigInt(pa[i] ?? "0");
|
|
524
|
+
const nb = BigInt(pb[i] ?? "0");
|
|
525
|
+
if (na < nb) return -1;
|
|
526
|
+
if (na > nb) return 1;
|
|
527
|
+
}
|
|
528
|
+
return 0;
|
|
529
|
+
}
|
|
530
|
+
const STORAGE_KEY = "feedbackkit.reporterId";
|
|
531
|
+
const VALID_ID = /^[A-Za-z0-9_-]{16,128}$/;
|
|
532
|
+
let memoryId = null;
|
|
533
|
+
let currentUser = null;
|
|
534
|
+
function freshId() {
|
|
535
|
+
const bytes = new Uint8Array(16);
|
|
536
|
+
crypto.getRandomValues(bytes);
|
|
537
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
538
|
+
}
|
|
539
|
+
function reporterId() {
|
|
540
|
+
if (memoryId) return memoryId;
|
|
541
|
+
try {
|
|
542
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
543
|
+
if (stored && VALID_ID.test(stored)) return memoryId = stored;
|
|
544
|
+
const id = freshId();
|
|
545
|
+
localStorage.setItem(STORAGE_KEY, id);
|
|
546
|
+
return memoryId = id;
|
|
547
|
+
} catch {
|
|
548
|
+
return memoryId = freshId();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function setUser(user) {
|
|
552
|
+
currentUser = user && (user.id || user.email || user.name) ? { ...user } : null;
|
|
553
|
+
}
|
|
554
|
+
function getUser() {
|
|
555
|
+
return currentUser;
|
|
556
|
+
}
|
|
557
|
+
function reporterUpdatesEndpoint(configuration2) {
|
|
558
|
+
if (configuration2.reporterUpdatesEndpoint) return configuration2.reporterUpdatesEndpoint;
|
|
559
|
+
const url = new URL(configuration2.endpoint ?? DEFAULT_ENDPOINT);
|
|
560
|
+
url.pathname = url.pathname.replace(/\/[^/]*\/?$/, "/reporter-updates");
|
|
561
|
+
url.search = "";
|
|
562
|
+
return url.toString();
|
|
563
|
+
}
|
|
564
|
+
function decodeUpdate(w) {
|
|
565
|
+
return {
|
|
566
|
+
feedbackId: w.feedback_id,
|
|
567
|
+
text: w.text ?? "",
|
|
568
|
+
createdAt: w.created_at ?? "",
|
|
569
|
+
screenName: w.screen_name ?? null,
|
|
570
|
+
fixStage: w.fix_stage ?? null,
|
|
571
|
+
fixedInBuild: w.fixed_in_build ?? null,
|
|
572
|
+
fixSummary: w.fix_summary ?? null,
|
|
573
|
+
needsVerification: w.needs_verification ?? false,
|
|
574
|
+
openQuestion: w.open_question ? { id: w.open_question.id, body: w.open_question.body, createdAt: w.open_question.created_at } : null,
|
|
575
|
+
screenshotUrl: w.screenshot_url ?? null,
|
|
576
|
+
messages: (w.messages ?? []).map((m) => ({ id: m.id, kind: m.kind, body: m.body, author: m.author, createdAt: m.created_at }))
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
async function fetchFixUpdates(configuration2) {
|
|
580
|
+
const url = new URL(reporterUpdatesEndpoint(configuration2));
|
|
581
|
+
url.searchParams.set("project_key", configuration2.projectKey);
|
|
582
|
+
url.searchParams.set("reporter_id", reporterId());
|
|
583
|
+
if (configuration2.appBuild) url.searchParams.set("build", configuration2.appBuild);
|
|
584
|
+
let response;
|
|
585
|
+
try {
|
|
586
|
+
response = await fetch(url.toString());
|
|
587
|
+
} catch (error) {
|
|
588
|
+
throw new FeedbackSubmissionError(`Network error: ${error instanceof Error ? error.message : String(error)}`, "network");
|
|
589
|
+
}
|
|
590
|
+
if (!response.ok) throw new FeedbackSubmissionError(`HTTP ${response.status}`, "server", response.status);
|
|
591
|
+
const json = await response.json();
|
|
592
|
+
return (json.updates ?? []).map(decodeUpdate);
|
|
593
|
+
}
|
|
594
|
+
async function encodeFixAction(configuration2, feedbackId, action) {
|
|
595
|
+
const body = {
|
|
596
|
+
project_key: configuration2.projectKey,
|
|
597
|
+
reporter_id: reporterId(),
|
|
598
|
+
feedback_id: feedbackId,
|
|
599
|
+
action: action.type
|
|
600
|
+
};
|
|
601
|
+
if (configuration2.appBuild) body.build = configuration2.appBuild;
|
|
602
|
+
if (action.type === "reply") body.text = action.text;
|
|
603
|
+
if (action.type === "reopen") {
|
|
604
|
+
const report = action.report;
|
|
605
|
+
const text2 = report?.text ?? action.text;
|
|
606
|
+
if (text2) body.text = text2;
|
|
607
|
+
if (report?.screenshotAnnotated) {
|
|
608
|
+
body.screenshot_annotated_png_base64 = await blobToBase64(report.screenshotAnnotated);
|
|
609
|
+
if (report.screenshotRaw) body.screenshot_raw_png_base64 = await blobToBase64(report.screenshotRaw);
|
|
610
|
+
body.annotations = report.annotations;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return body;
|
|
614
|
+
}
|
|
615
|
+
async function sendFixAction(configuration2, feedbackId, action) {
|
|
616
|
+
const body = JSON.stringify(await encodeFixAction(configuration2, feedbackId, action));
|
|
617
|
+
let response;
|
|
618
|
+
try {
|
|
619
|
+
response = await fetch(reporterUpdatesEndpoint(configuration2), {
|
|
620
|
+
method: "POST",
|
|
621
|
+
headers: { "Content-Type": "application/json" },
|
|
622
|
+
body
|
|
623
|
+
});
|
|
624
|
+
} catch (error) {
|
|
625
|
+
throw new FeedbackSubmissionError(`Network error: ${error instanceof Error ? error.message : String(error)}`, "network");
|
|
626
|
+
}
|
|
627
|
+
if (!response.ok) throw new FeedbackSubmissionError(`HTTP ${response.status}`, "server", response.status);
|
|
628
|
+
}
|
|
629
|
+
const svg = (body) => `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
|
630
|
+
const icons = {
|
|
631
|
+
feedback: svg(
|
|
632
|
+
'<path d="M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12Z"/><path d="M8.5 10.5h7M8.5 14h4.5"/>'
|
|
633
|
+
),
|
|
634
|
+
pen: svg('<path d="M4 20l4-1L19 8a2.1 2.1 0 0 0-3-3L5 16l-1 4Z"/><path d="M14.5 6.5l3 3"/>'),
|
|
635
|
+
rectangle: svg('<rect x="4" y="6" width="16" height="12" rx="1.5"/>'),
|
|
636
|
+
arrow: svg('<path d="M6 18L18 6"/><path d="M9 6h9v9"/>'),
|
|
637
|
+
text: svg('<path d="M5 6V5h14v1"/><path d="M12 5v14"/><path d="M9 19h6"/>'),
|
|
638
|
+
move: svg(
|
|
639
|
+
'<path d="M12 3v18M3 12h18"/><path d="M9 6l3-3 3 3M9 18l3 3 3-3M6 9l-3 3 3 3M18 9l3 3-3 3"/>'
|
|
640
|
+
),
|
|
641
|
+
undo: svg('<path d="M9 14L4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>'),
|
|
642
|
+
trash: svg('<path d="M4 7h16"/><path d="M10 11v6M14 11v6"/><path d="M6 7l1 13h10l1-13"/><path d="M9 7V4h6v3"/>'),
|
|
643
|
+
close: svg('<path d="M6 6l12 12M18 6L6 18"/>'),
|
|
644
|
+
paperclip: svg(
|
|
645
|
+
'<path d="M21 11.5l-8.6 8.6a5.5 5.5 0 0 1-7.8-7.8l8.6-8.6a3.7 3.7 0 0 1 5.2 5.2l-8.6 8.6a1.8 1.8 0 0 1-2.6-2.6l7.9-7.9"/>'
|
|
646
|
+
),
|
|
647
|
+
send: svg('<path d="M4 12l16-8-6 16-3-7-7-1Z"/>'),
|
|
648
|
+
check: svg('<path d="M5 12.5l4.5 4.5L19 7.5"/>')
|
|
649
|
+
};
|
|
650
|
+
class FixCard {
|
|
651
|
+
constructor(shadow, update, onOutcome) {
|
|
652
|
+
this.shadow = shadow;
|
|
653
|
+
this.update = update;
|
|
654
|
+
this.onOutcome = onOutcome;
|
|
655
|
+
this.root = null;
|
|
656
|
+
this.settled = false;
|
|
657
|
+
}
|
|
658
|
+
show(position) {
|
|
659
|
+
const u = this.update;
|
|
660
|
+
const card = el$1("section", "fk-fixcard");
|
|
661
|
+
card.dataset.position = position;
|
|
662
|
+
card.setAttribute("role", "dialog");
|
|
663
|
+
card.setAttribute("aria-labelledby", "fk-fixcard-title");
|
|
664
|
+
const header = el$1("div", "fk-fixcard-header");
|
|
665
|
+
const title = el$1("h2", "fk-title");
|
|
666
|
+
title.id = "fk-fixcard-title";
|
|
667
|
+
title.textContent = u.needsVerification ? "We fixed something you reported" : "A question about your report";
|
|
668
|
+
const close = document.createElement("button");
|
|
669
|
+
close.type = "button";
|
|
670
|
+
close.className = "fk-close";
|
|
671
|
+
close.innerHTML = icons.close;
|
|
672
|
+
close.setAttribute("aria-label", "Remind me later");
|
|
673
|
+
close.title = "Remind me later";
|
|
674
|
+
close.addEventListener("click", () => this.settle({ type: "later" }));
|
|
675
|
+
header.append(title, close);
|
|
676
|
+
card.append(header);
|
|
677
|
+
if (u.needsVerification && u.fixedInBuild) {
|
|
678
|
+
card.append(text("p", "fk-meta", `Fixed in build ${u.fixedInBuild}.`));
|
|
679
|
+
}
|
|
680
|
+
if (u.screenshotUrl) {
|
|
681
|
+
const img = document.createElement("img");
|
|
682
|
+
img.className = "fk-fixcard-shot";
|
|
683
|
+
img.alt = "Your screenshot from the original report";
|
|
684
|
+
img.src = u.screenshotUrl;
|
|
685
|
+
img.addEventListener("error", () => img.remove());
|
|
686
|
+
card.append(img);
|
|
687
|
+
}
|
|
688
|
+
card.append(text("p", "fk-meta", "You reported"));
|
|
689
|
+
card.append(text("p", "fk-fixcard-quote", u.text ? `“${u.text}”` : "(no description)"));
|
|
690
|
+
if (u.needsVerification && u.fixSummary) {
|
|
691
|
+
card.append(text("p", "fk-fixcard-summary", `What changed: ${u.fixSummary}`));
|
|
692
|
+
}
|
|
693
|
+
for (const m of u.messages.filter((m2) => m2.kind === "comment")) {
|
|
694
|
+
const msg = el$1("div", "fk-fixcard-message");
|
|
695
|
+
msg.append(text("span", "fk-meta", m.author), text("p", "", m.body));
|
|
696
|
+
card.append(msg);
|
|
697
|
+
}
|
|
698
|
+
const actions = el$1("div", "fk-fixcard-actions");
|
|
699
|
+
if (u.needsVerification) {
|
|
700
|
+
const yes = button("fk-btn fk-btn-primary", "Yes, it's fixed");
|
|
701
|
+
yes.addEventListener("click", () => this.settle({ type: "verified" }));
|
|
702
|
+
const no = button("fk-btn fk-btn-secondary", "No, still broken");
|
|
703
|
+
no.addEventListener("click", () => this.settle({ type: "stillBroken" }));
|
|
704
|
+
actions.append(yes, no);
|
|
705
|
+
} else if (u.openQuestion) {
|
|
706
|
+
actions.append(text("p", "fk-fixcard-question", u.openQuestion.body));
|
|
707
|
+
const input = document.createElement("textarea");
|
|
708
|
+
input.className = "fk-textarea";
|
|
709
|
+
input.placeholder = "Your answer";
|
|
710
|
+
input.setAttribute("aria-label", "Your answer");
|
|
711
|
+
const send = button("fk-btn fk-btn-primary", "Send");
|
|
712
|
+
send.disabled = true;
|
|
713
|
+
input.addEventListener("input", () => send.disabled = input.value.trim() === "");
|
|
714
|
+
send.addEventListener("click", () => this.settle({ type: "replied", text: input.value.trim() }));
|
|
715
|
+
actions.append(input, send);
|
|
716
|
+
}
|
|
717
|
+
card.append(actions);
|
|
718
|
+
this.root = card;
|
|
719
|
+
this.shadow.append(card);
|
|
720
|
+
}
|
|
721
|
+
/** Close as "later" (e.g. `FeedbackKit.disableFixVerification()`). */
|
|
722
|
+
dismiss() {
|
|
723
|
+
this.settle({ type: "later" });
|
|
724
|
+
}
|
|
725
|
+
/** Remove without reporting an outcome (the widget is being destroyed). */
|
|
726
|
+
remove() {
|
|
727
|
+
this.settled = true;
|
|
728
|
+
this.root?.remove();
|
|
729
|
+
this.root = null;
|
|
730
|
+
}
|
|
731
|
+
settle(outcome) {
|
|
732
|
+
if (this.settled) return;
|
|
733
|
+
this.settled = true;
|
|
734
|
+
this.root?.remove();
|
|
735
|
+
this.root = null;
|
|
736
|
+
this.onOutcome(outcome);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function el$1(tag, className) {
|
|
740
|
+
const node = document.createElement(tag);
|
|
741
|
+
if (className) node.className = className;
|
|
742
|
+
return node;
|
|
743
|
+
}
|
|
744
|
+
function text(tag, className, content) {
|
|
745
|
+
const node = el$1(tag, className);
|
|
746
|
+
node.textContent = content;
|
|
747
|
+
return node;
|
|
748
|
+
}
|
|
749
|
+
function button(className, label) {
|
|
750
|
+
const b = el$1("button", className);
|
|
751
|
+
b.type = "button";
|
|
752
|
+
b.textContent = label;
|
|
753
|
+
return b;
|
|
754
|
+
}
|
|
513
755
|
const STROKE_WIDTH = 4;
|
|
514
756
|
const HIT_TEST_TOLERANCE = 16;
|
|
515
757
|
const HEAD_LENGTH = 18;
|
|
@@ -628,8 +870,8 @@ function arrowGeometry(rawStart, rawEnd, scale = 1, rotation = 0) {
|
|
|
628
870
|
};
|
|
629
871
|
return { start, end, left, right };
|
|
630
872
|
}
|
|
631
|
-
function drawText(ctx,
|
|
632
|
-
const rect = textBubbleRect(ctx,
|
|
873
|
+
function drawText(ctx, text2, point, color, scale) {
|
|
874
|
+
const rect = textBubbleRect(ctx, text2, point, scale);
|
|
633
875
|
ctx.save();
|
|
634
876
|
ctx.fillStyle = color;
|
|
635
877
|
ctx.beginPath();
|
|
@@ -638,7 +880,7 @@ function drawText(ctx, text, point, color, scale) {
|
|
|
638
880
|
ctx.font = textFont(scale);
|
|
639
881
|
ctx.fillStyle = "#FFFFFF";
|
|
640
882
|
ctx.textBaseline = "top";
|
|
641
|
-
ctx.fillText(
|
|
883
|
+
ctx.fillText(text2, rect.x + TEXT_PADDING, rect.y + TEXT_PADDING + BASE_FONT_SIZE * scale * (LINE_HEIGHT_RATIO - 1) / 2);
|
|
642
884
|
ctx.restore();
|
|
643
885
|
}
|
|
644
886
|
function roundedRectPath(ctx, x, y, w, h, r) {
|
|
@@ -650,10 +892,10 @@ function roundedRectPath(ctx, x, y, w, h, r) {
|
|
|
650
892
|
ctx.arcTo(x, y, x + w, y, radius);
|
|
651
893
|
ctx.closePath();
|
|
652
894
|
}
|
|
653
|
-
function textBubbleRect(measurer,
|
|
895
|
+
function textBubbleRect(measurer, text2, point, scale = 1) {
|
|
654
896
|
const previous = measurer.font;
|
|
655
897
|
measurer.font = textFont(scale);
|
|
656
|
-
const width = measurer.measureText(
|
|
898
|
+
const width = measurer.measureText(text2).width;
|
|
657
899
|
measurer.font = previous;
|
|
658
900
|
const height = BASE_FONT_SIZE * scale * LINE_HEIGHT_RATIO;
|
|
659
901
|
const padding = TEXT_PADDING * scale;
|
|
@@ -1055,27 +1297,6 @@ function clamp(v, lo, hi) {
|
|
|
1055
1297
|
function round4(v) {
|
|
1056
1298
|
return Math.round(v * 1e4) / 1e4;
|
|
1057
1299
|
}
|
|
1058
|
-
const svg = (body) => `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
|
1059
|
-
const icons = {
|
|
1060
|
-
feedback: svg(
|
|
1061
|
-
'<path d="M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12Z"/><path d="M8.5 10.5h7M8.5 14h4.5"/>'
|
|
1062
|
-
),
|
|
1063
|
-
pen: svg('<path d="M4 20l4-1L19 8a2.1 2.1 0 0 0-3-3L5 16l-1 4Z"/><path d="M14.5 6.5l3 3"/>'),
|
|
1064
|
-
rectangle: svg('<rect x="4" y="6" width="16" height="12" rx="1.5"/>'),
|
|
1065
|
-
arrow: svg('<path d="M6 18L18 6"/><path d="M9 6h9v9"/>'),
|
|
1066
|
-
text: svg('<path d="M5 6V5h14v1"/><path d="M12 5v14"/><path d="M9 19h6"/>'),
|
|
1067
|
-
move: svg(
|
|
1068
|
-
'<path d="M12 3v18M3 12h18"/><path d="M9 6l3-3 3 3M9 18l3 3 3-3M6 9l-3 3 3 3M18 9l3 3-3 3"/>'
|
|
1069
|
-
),
|
|
1070
|
-
undo: svg('<path d="M9 14L4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>'),
|
|
1071
|
-
trash: svg('<path d="M4 7h16"/><path d="M10 11v6M14 11v6"/><path d="M6 7l1 13h10l1-13"/><path d="M9 7V4h6v3"/>'),
|
|
1072
|
-
close: svg('<path d="M6 6l12 12M18 6L6 18"/>'),
|
|
1073
|
-
paperclip: svg(
|
|
1074
|
-
'<path d="M21 11.5l-8.6 8.6a5.5 5.5 0 0 1-7.8-7.8l8.6-8.6a3.7 3.7 0 0 1 5.2 5.2l-8.6 8.6a1.8 1.8 0 0 1-2.6-2.6l7.9-7.9"/>'
|
|
1075
|
-
),
|
|
1076
|
-
send: svg('<path d="M4 12l16-8-6 16-3-7-7-1Z"/>'),
|
|
1077
|
-
check: svg('<path d="M5 12.5l4.5 4.5L19 7.5"/>')
|
|
1078
|
-
};
|
|
1079
1300
|
const styles = (
|
|
1080
1301
|
/* css */
|
|
1081
1302
|
`
|
|
@@ -1332,6 +1553,39 @@ button:disabled { cursor: not-allowed; opacity: 0.45; }
|
|
|
1332
1553
|
.fk-done-icon svg { width: 26px; height: 26px; }
|
|
1333
1554
|
.fk-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
1334
1555
|
|
|
1556
|
+
/* ---------- "is it fixed?" card (fixCard.ts) ---------- */
|
|
1557
|
+
.fk-fixcard {
|
|
1558
|
+
position: fixed;
|
|
1559
|
+
z-index: 2147483000;
|
|
1560
|
+
right: 20px;
|
|
1561
|
+
bottom: 76px;
|
|
1562
|
+
width: min(360px, calc(100vw - 32px));
|
|
1563
|
+
max-height: min(640px, calc(100vh - 96px));
|
|
1564
|
+
overflow: auto;
|
|
1565
|
+
display: flex;
|
|
1566
|
+
flex-direction: column;
|
|
1567
|
+
gap: 8px;
|
|
1568
|
+
padding: 16px;
|
|
1569
|
+
border: 1px solid var(--fk-border);
|
|
1570
|
+
border-radius: 16px;
|
|
1571
|
+
background: var(--fk-bg);
|
|
1572
|
+
box-shadow: var(--fk-shadow);
|
|
1573
|
+
animation: fk-rise 0.2s ease-out;
|
|
1574
|
+
}
|
|
1575
|
+
.fk-fixcard[data-position="bottom-left"] { right: auto; left: 20px; }
|
|
1576
|
+
.fk-fixcard > *, .fk-fixcard-actions > * { flex-shrink: 0; }
|
|
1577
|
+
.fk-fixcard p { margin: 0; }
|
|
1578
|
+
.fk-fixcard-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
|
1579
|
+
.fk-fixcard-shot { display: block; width: 100%; max-height: 220px; object-fit: contain; border-radius: 10px; background: var(--fk-surface); }
|
|
1580
|
+
.fk-fixcard-quote { font-size: 14px; }
|
|
1581
|
+
.fk-fixcard-summary { font-size: 13px; padding: 8px 10px; border-radius: 8px; background: var(--fk-surface); }
|
|
1582
|
+
.fk-fixcard-message p { font-size: 13px; }
|
|
1583
|
+
.fk-fixcard-question { font-weight: 600; }
|
|
1584
|
+
.fk-fixcard-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
|
|
1585
|
+
.fk-fixcard-actions .fk-btn { flex: none; width: 100%; }
|
|
1586
|
+
.fk-fixcard-actions .fk-textarea { min-height: 64px; }
|
|
1587
|
+
@keyframes fk-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
|
1588
|
+
|
|
1335
1589
|
/* ---------- narrow screens: stack stage over composer ---------- */
|
|
1336
1590
|
@media (max-width: 760px) {
|
|
1337
1591
|
.fk-overlay { padding: 0; }
|
|
@@ -1341,9 +1595,10 @@ button:disabled { cursor: not-allowed; opacity: 0.45; }
|
|
|
1341
1595
|
.fk-canvas-wrap { padding: 10px; }
|
|
1342
1596
|
.fk-composer { max-height: 55vh; padding: 14px; gap: 10px; }
|
|
1343
1597
|
.fk-textarea { min-height: 84px; }
|
|
1598
|
+
.fk-fixcard, .fk-fixcard[data-position] { right: 16px; left: 16px; width: auto; bottom: 16px; }
|
|
1344
1599
|
}
|
|
1345
1600
|
@media (prefers-reduced-motion: reduce) {
|
|
1346
|
-
.fk-overlay, .fk-dialog { animation: none; }
|
|
1601
|
+
.fk-overlay, .fk-dialog, .fk-fixcard { animation: none; }
|
|
1347
1602
|
}
|
|
1348
1603
|
`
|
|
1349
1604
|
);
|
|
@@ -1369,26 +1624,48 @@ class Widget {
|
|
|
1369
1624
|
this.shadow = null;
|
|
1370
1625
|
this.trigger = null;
|
|
1371
1626
|
this.pending = null;
|
|
1627
|
+
this.fixCard = null;
|
|
1372
1628
|
this.isOwnNode = (node) => node === this.host;
|
|
1373
1629
|
}
|
|
1630
|
+
get isShowingFixCard() {
|
|
1631
|
+
return this.fixCard !== null;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* The non-modal "is it fixed?" card (bottom corner). Resolves with what
|
|
1635
|
+
* the reporter chose; the card removes itself.
|
|
1636
|
+
*/
|
|
1637
|
+
showFixCard(update) {
|
|
1638
|
+
this.fixCard?.remove();
|
|
1639
|
+
const shadow = this.mount();
|
|
1640
|
+
return new Promise((resolve) => {
|
|
1641
|
+
this.fixCard = new FixCard(shadow, update, (outcome) => {
|
|
1642
|
+
this.fixCard = null;
|
|
1643
|
+
resolve(outcome);
|
|
1644
|
+
});
|
|
1645
|
+
this.fixCard.show(this.trigger?.dataset.position === "bottom-left" ? "bottom-left" : "bottom-right");
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
hideFixCard() {
|
|
1649
|
+
this.fixCard?.dismiss();
|
|
1650
|
+
}
|
|
1374
1651
|
get isOpen() {
|
|
1375
1652
|
return this.pending !== null;
|
|
1376
1653
|
}
|
|
1377
1654
|
showTrigger(options, onClick) {
|
|
1378
1655
|
const shadow = this.mount();
|
|
1379
1656
|
this.trigger?.remove();
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1657
|
+
const button2 = document.createElement("button");
|
|
1658
|
+
button2.type = "button";
|
|
1659
|
+
button2.className = "fk-trigger";
|
|
1660
|
+
button2.dataset.position = options.position ?? "bottom-right";
|
|
1661
|
+
button2.dataset.compact = String(!!options.compact);
|
|
1385
1662
|
const label = options.label ?? "Feedback";
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
shadow.append(
|
|
1391
|
-
this.trigger =
|
|
1663
|
+
button2.setAttribute("aria-label", label);
|
|
1664
|
+
button2.innerHTML = `${icons.feedback}<span class="fk-trigger-label"></span>`;
|
|
1665
|
+
button2.querySelector(".fk-trigger-label").textContent = label;
|
|
1666
|
+
button2.addEventListener("click", onClick);
|
|
1667
|
+
shadow.append(button2);
|
|
1668
|
+
this.trigger = button2;
|
|
1392
1669
|
this.applyTheme();
|
|
1393
1670
|
}
|
|
1394
1671
|
hideTrigger() {
|
|
@@ -1397,6 +1674,8 @@ class Widget {
|
|
|
1397
1674
|
}
|
|
1398
1675
|
destroy() {
|
|
1399
1676
|
this.hideTrigger();
|
|
1677
|
+
this.fixCard?.remove();
|
|
1678
|
+
this.fixCard = null;
|
|
1400
1679
|
this.host?.remove();
|
|
1401
1680
|
this.host = null;
|
|
1402
1681
|
this.shadow = null;
|
|
@@ -1580,17 +1859,17 @@ class Dialog {
|
|
|
1580
1859
|
const header = el("div", "fk-header");
|
|
1581
1860
|
const title = el("h2", "fk-title");
|
|
1582
1861
|
title.id = "fk-title";
|
|
1583
|
-
title.textContent = "Send feedback";
|
|
1862
|
+
title.textContent = this.options.copy?.title ?? "Send feedback";
|
|
1584
1863
|
const close = iconButton("fk-close", icons.close, "Close");
|
|
1585
1864
|
close.addEventListener("click", () => this.cancel());
|
|
1586
1865
|
header.append(title, close);
|
|
1587
1866
|
const textLabel = el("label", "fk-label");
|
|
1588
|
-
textLabel.textContent = "What's the problem?";
|
|
1867
|
+
textLabel.textContent = this.options.copy?.label ?? "What's the problem?";
|
|
1589
1868
|
textLabel.setAttribute("for", "fk-text");
|
|
1590
1869
|
this.textarea = document.createElement("textarea");
|
|
1591
1870
|
this.textarea.id = "fk-text";
|
|
1592
1871
|
this.textarea.className = "fk-textarea";
|
|
1593
|
-
this.textarea.placeholder = "Describe what happened and what you expected instead…";
|
|
1872
|
+
this.textarea.placeholder = this.options.copy?.placeholder ?? "Describe what happened and what you expected instead…";
|
|
1594
1873
|
this.textarea.addEventListener("input", () => this.refreshState());
|
|
1595
1874
|
const textGroup = el("div");
|
|
1596
1875
|
textGroup.append(textLabel, this.textarea);
|
|
@@ -1816,14 +2095,16 @@ class Dialog {
|
|
|
1816
2095
|
this.sendButton.innerHTML = `<span class="fk-spinner" aria-hidden="true"></span><span>Sending…</span>`;
|
|
1817
2096
|
try {
|
|
1818
2097
|
const report = await this.buildReport();
|
|
1819
|
-
if (this.options.
|
|
2098
|
+
if (this.options.deliver) {
|
|
2099
|
+
await this.options.deliver(report);
|
|
2100
|
+
} else if (this.options.submit) {
|
|
1820
2101
|
const configuration2 = this.deps.configuration();
|
|
1821
2102
|
if (!configuration2) throw new Error("FeedbackKit.configure() hasn't been called.");
|
|
1822
|
-
await submitReport(report, configuration2);
|
|
2103
|
+
await submitReport(report, configuration2, this.deps.identity());
|
|
1823
2104
|
}
|
|
1824
2105
|
await this.options.onReport?.(report);
|
|
1825
2106
|
this.showThanks();
|
|
1826
|
-
setTimeout(() => this.finish(report), this.
|
|
2107
|
+
setTimeout(() => this.finish(report), this.delivers ? 1300 : 0);
|
|
1827
2108
|
} catch (error) {
|
|
1828
2109
|
this.sending = false;
|
|
1829
2110
|
this.sendButton.innerHTML = `${icons.send}<span>Retry</span>`;
|
|
@@ -1832,8 +2113,11 @@ class Dialog {
|
|
|
1832
2113
|
this.options.onError?.(error);
|
|
1833
2114
|
}
|
|
1834
2115
|
}
|
|
2116
|
+
get delivers() {
|
|
2117
|
+
return this.options.submit || !!this.options.deliver;
|
|
2118
|
+
}
|
|
1835
2119
|
showThanks() {
|
|
1836
|
-
if (!this.
|
|
2120
|
+
if (!this.delivers) return;
|
|
1837
2121
|
const composer = this.overlay.querySelector(".fk-composer");
|
|
1838
2122
|
if (!composer) return;
|
|
1839
2123
|
const done = el("div", "fk-done");
|
|
@@ -1841,9 +2125,9 @@ class Dialog {
|
|
|
1841
2125
|
const icon = el("span", "fk-done-icon");
|
|
1842
2126
|
icon.innerHTML = icons.check;
|
|
1843
2127
|
const title = el("h2", "fk-title");
|
|
1844
|
-
title.textContent = "Thanks for the feedback!";
|
|
2128
|
+
title.textContent = this.options.copy?.thanksTitle ?? "Thanks for the feedback!";
|
|
1845
2129
|
const sub = el("p", "fk-meta");
|
|
1846
|
-
sub.textContent = "It's on its way to the team.";
|
|
2130
|
+
sub.textContent = this.options.copy?.thanksBody ?? "It's on its way to the team.";
|
|
1847
2131
|
done.append(icon, title, sub);
|
|
1848
2132
|
composer.replaceChildren(done);
|
|
1849
2133
|
}
|
|
@@ -1884,9 +2168,9 @@ function switchControl(label, checked, onChange) {
|
|
|
1884
2168
|
input.setAttribute("role", "switch");
|
|
1885
2169
|
input.addEventListener("change", () => onChange(input.checked));
|
|
1886
2170
|
const track = el("span", "fk-switch-track");
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
wrap.append(input, track,
|
|
2171
|
+
const text2 = el("span");
|
|
2172
|
+
text2.textContent = label;
|
|
2173
|
+
wrap.append(input, track, text2);
|
|
1890
2174
|
return wrap;
|
|
1891
2175
|
}
|
|
1892
2176
|
function validHex(hex) {
|
|
@@ -1915,8 +2199,66 @@ const widget = new Widget({
|
|
|
1915
2199
|
defaultProductKey: () => configuration?.defaultProductKey,
|
|
1916
2200
|
logs: () => logCapture?.buffer.snapshot() ?? [],
|
|
1917
2201
|
captureOptions: () => FeedbackKit.captureOptions,
|
|
1918
|
-
currentScreen: () => FeedbackKit.currentScreen
|
|
2202
|
+
currentScreen: () => FeedbackKit.currentScreen,
|
|
2203
|
+
identity: () => ({ reporterId: reporterId(), user: getUser() })
|
|
1919
2204
|
});
|
|
2205
|
+
const FIX_CHECK_INTERVAL_MS = 6e4;
|
|
2206
|
+
let fixVerificationEnabled = false;
|
|
2207
|
+
let lastFixCheck = 0;
|
|
2208
|
+
let fixCheckInFlight = false;
|
|
2209
|
+
const handledThisPage = /* @__PURE__ */ new Set();
|
|
2210
|
+
function onVisibilityChange() {
|
|
2211
|
+
if (document.visibilityState === "visible") void checkAndShowFix(false);
|
|
2212
|
+
}
|
|
2213
|
+
async function checkAndShowFix(force) {
|
|
2214
|
+
const config = configuration;
|
|
2215
|
+
if (!fixVerificationEnabled || !config || fixCheckInFlight || widget.isOpen || widget.isShowingFixCard) return;
|
|
2216
|
+
if (!force && Date.now() - lastFixCheck < FIX_CHECK_INTERVAL_MS) return;
|
|
2217
|
+
fixCheckInFlight = true;
|
|
2218
|
+
lastFixCheck = Date.now();
|
|
2219
|
+
let next;
|
|
2220
|
+
try {
|
|
2221
|
+
next = (await fetchFixUpdates(config)).find((u) => !handledThisPage.has(u.feedbackId));
|
|
2222
|
+
} catch {
|
|
2223
|
+
return;
|
|
2224
|
+
} finally {
|
|
2225
|
+
fixCheckInFlight = false;
|
|
2226
|
+
}
|
|
2227
|
+
if (!next || !fixVerificationEnabled) return;
|
|
2228
|
+
void handleFixCard(config, next);
|
|
2229
|
+
}
|
|
2230
|
+
async function handleFixCard(config, update) {
|
|
2231
|
+
const outcome = await widget.showFixCard(update);
|
|
2232
|
+
handledThisPage.add(update.feedbackId);
|
|
2233
|
+
const send = (action) => sendFixAction(config, update.feedbackId, action).catch(
|
|
2234
|
+
(error) => console.warn("[FeedbackKit] Couldn't send fix verification.", error)
|
|
2235
|
+
);
|
|
2236
|
+
switch (outcome.type) {
|
|
2237
|
+
case "verified":
|
|
2238
|
+
await send({ type: "verify" });
|
|
2239
|
+
void checkAndShowFix(true);
|
|
2240
|
+
break;
|
|
2241
|
+
case "replied":
|
|
2242
|
+
await send({ type: "reply", text: outcome.text });
|
|
2243
|
+
void checkAndShowFix(true);
|
|
2244
|
+
break;
|
|
2245
|
+
case "stillBroken": {
|
|
2246
|
+
const report = await widget.open({
|
|
2247
|
+
submit: false,
|
|
2248
|
+
deliver: (r) => sendFixAction(config, update.feedbackId, { type: "reopen", report: r }),
|
|
2249
|
+
copy: {
|
|
2250
|
+
title: "Still broken",
|
|
2251
|
+
label: "What's still wrong?",
|
|
2252
|
+
placeholder: "Show us on the screenshot and describe what you still see…",
|
|
2253
|
+
thanksTitle: "Thanks — we're on it",
|
|
2254
|
+
thanksBody: "Your report was reopened and sent back to the team."
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
if (!report) await send({ type: "reopen" });
|
|
2258
|
+
break;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
1920
2262
|
function resolveProducts() {
|
|
1921
2263
|
if (products.length > 0 || !configuration) return Promise.resolve(products);
|
|
1922
2264
|
if (!productsPromise) {
|
|
@@ -1995,7 +2337,7 @@ const FeedbackKit = {
|
|
|
1995
2337
|
if (!configuration) {
|
|
1996
2338
|
return Promise.reject(new Error("FeedbackKit.configure({ projectKey }) must be called before submit()."));
|
|
1997
2339
|
}
|
|
1998
|
-
return submitReport(report, configuration);
|
|
2340
|
+
return submitReport(report, configuration, { reporterId: reporterId(), user: getUser() });
|
|
1999
2341
|
},
|
|
2000
2342
|
get isPresenting() {
|
|
2001
2343
|
return widget.isOpen;
|
|
@@ -2049,8 +2391,69 @@ const FeedbackKit = {
|
|
|
2049
2391
|
const shot = await captureViewport({ ...FeedbackKit.captureOptions, ...options, exclude: widget.isOwnNode });
|
|
2050
2392
|
return canvasToPngBlob(shot.canvas);
|
|
2051
2393
|
},
|
|
2394
|
+
/**
|
|
2395
|
+
* Optional identity of the person using your site, attached to every
|
|
2396
|
+
* report they submit. Not needed for fix verification (that uses an
|
|
2397
|
+
* anonymous per-browser id, `reporterId`). Pass `null` on sign-out.
|
|
2398
|
+
*/
|
|
2399
|
+
setUser(user) {
|
|
2400
|
+
setUser(user);
|
|
2401
|
+
},
|
|
2402
|
+
/** The anonymous per-browser id attached to reports — how a fix finds its way back here. */
|
|
2403
|
+
get reporterId() {
|
|
2404
|
+
return reporterId();
|
|
2405
|
+
},
|
|
2406
|
+
/**
|
|
2407
|
+
* This browser's reports that need the reporter's attention: shipped
|
|
2408
|
+
* fixes to confirm and developer/agent questions. For a custom UI — the
|
|
2409
|
+
* built-in one is `enableFixVerification`.
|
|
2410
|
+
*/
|
|
2411
|
+
checkForFixUpdates() {
|
|
2412
|
+
if (!configuration) {
|
|
2413
|
+
return Promise.reject(new Error("FeedbackKit.configure({ projectKey }) must be called before checkForFixUpdates()."));
|
|
2414
|
+
}
|
|
2415
|
+
return fetchFixUpdates(configuration);
|
|
2416
|
+
},
|
|
2417
|
+
/** Answers one update directly (custom UIs): verify, reopen, or reply. */
|
|
2418
|
+
respondToFixUpdate(feedbackId, action) {
|
|
2419
|
+
if (!configuration) {
|
|
2420
|
+
return Promise.reject(new Error("FeedbackKit.configure({ projectKey }) must be called before respondToFixUpdate()."));
|
|
2421
|
+
}
|
|
2422
|
+
return sendFixAction(configuration, feedbackId, action);
|
|
2423
|
+
},
|
|
2424
|
+
/**
|
|
2425
|
+
* Closes the loop with the person who reported a bug: once a fix for one
|
|
2426
|
+
* of their reports ships (announced with `feedbackkit release`), a small
|
|
2427
|
+
* card shows their original screenshot and asks "is it fixed?". "Still
|
|
2428
|
+
* broken" reopens the capture dialog so they can show what's wrong now,
|
|
2429
|
+
* and the report goes straight back to the developer. Also surfaces
|
|
2430
|
+
* questions the developer or their coding agent asked.
|
|
2431
|
+
*
|
|
2432
|
+
* Checks now and whenever the tab becomes visible (at most once a minute).
|
|
2433
|
+
* With `appBuild` configured, only fixes shipped in that build or earlier
|
|
2434
|
+
* are shown; without it, a fix counts as live as soon as it's released.
|
|
2435
|
+
*/
|
|
2436
|
+
enableFixVerification() {
|
|
2437
|
+
if (fixVerificationEnabled) return;
|
|
2438
|
+
fixVerificationEnabled = true;
|
|
2439
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
2440
|
+
whenBodyReady(() => void checkAndShowFix(true));
|
|
2441
|
+
},
|
|
2442
|
+
disableFixVerification() {
|
|
2443
|
+
fixVerificationEnabled = false;
|
|
2444
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
2445
|
+
widget.hideFixCard();
|
|
2446
|
+
},
|
|
2447
|
+
/**
|
|
2448
|
+
* Checks right now (ignoring the throttle) and shows the card if there's
|
|
2449
|
+
* something. Resolves once the check is done and any card is on screen.
|
|
2450
|
+
*/
|
|
2451
|
+
presentFixUpdatesIfNeeded() {
|
|
2452
|
+
return checkAndShowFix(true);
|
|
2453
|
+
},
|
|
2052
2454
|
/** Removes every trace of the SDK from the page (trigger, dialog host, listeners, log hooks). */
|
|
2053
2455
|
destroy() {
|
|
2456
|
+
FeedbackKit.disableFixVerification();
|
|
2054
2457
|
FeedbackKit.disableKeyboardShortcut();
|
|
2055
2458
|
logCapture?.uninstall();
|
|
2056
2459
|
logCapture = null;
|
|
@@ -2062,6 +2465,7 @@ export {
|
|
|
2062
2465
|
FeedbackKit,
|
|
2063
2466
|
FeedbackSubmissionError,
|
|
2064
2467
|
STROKE_WIDTH,
|
|
2468
|
+
compareBuilds,
|
|
2065
2469
|
FeedbackKit as default,
|
|
2066
2470
|
drawAnnotation,
|
|
2067
2471
|
drawAnnotations,
|
|
@@ -2070,6 +2474,7 @@ export {
|
|
|
2070
2474
|
parseUserAgent,
|
|
2071
2475
|
readPoint,
|
|
2072
2476
|
redactUrl,
|
|
2477
|
+
reporterUpdatesEndpoint,
|
|
2073
2478
|
textBubbleRect
|
|
2074
2479
|
};
|
|
2075
2480
|
//# sourceMappingURL=feedbackkit.js.map
|