@goflowstate/widget-sdk 0.3.4 → 0.3.5
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 +5 -2
- package/dist/canvas-widget-sdk.js +174 -47
- package/dist/canvas-widget-sdk.js.map +2 -2
- package/dist/feedback.d.ts +5 -3
- package/dist/feedback.d.ts.map +1 -1
- package/dist/index.mjs +174 -47
- package/dist/index.mjs.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -161,8 +161,11 @@ before the first state arrives.
|
|
|
161
161
|
A flow that calls `CanvasWidget.initSession()` carries the feedback icon with no code of its own.
|
|
162
162
|
While the ring has feedback switched on, the session state says so and the SDK puts a round
|
|
163
163
|
button in the corner of the page. A tap opens a sheet with one field at once and takes a
|
|
164
|
-
screenshot of the page behind it
|
|
165
|
-
|
|
164
|
+
screenshot of the page behind it, and neither the icon nor the sheet is in it. The sheet says each
|
|
165
|
+
step: corner marks frame the page and the slot reads "Taking a screenshot" (up to 15 seconds),
|
|
166
|
+
then "Screenshot attached" with the picture and a check, or "No screenshot" with "Take
|
|
167
|
+
screenshot" when it could not be taken or was removed. The button reads "Sending" while the note
|
|
168
|
+
goes out, and the sheet ends on "Feedback sent" before it closes. The note is sent through the flow's session to `POST /api/w/:slug/feedback`.
|
|
166
169
|
|
|
167
170
|
The icon and the sheet take the look of the page they sit on: its fonts, ground, ink, the fill
|
|
168
171
|
and shape of its main button, and its corner radius. To pin a role, set a custom property on
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @goflowstate/widget-sdk v0.3.
|
|
1
|
+
// @goflowstate/widget-sdk v0.3.5
|
|
2
2
|
// https://github.com/goflowstate/goflowstate-sdk
|
|
3
3
|
|
|
4
4
|
"use strict";
|
|
@@ -81,7 +81,9 @@ var CanvasWidgetSDK = (() => {
|
|
|
81
81
|
var FEEDBACK_HOST_ATTR = "data-fs-feedback-host";
|
|
82
82
|
var SCREENSHOT_MAX_BYTES = 15e4;
|
|
83
83
|
var SCREENSHOT_MAX_SIDE = 1280;
|
|
84
|
-
var CAPTURE_TIMEOUT_MS =
|
|
84
|
+
var CAPTURE_TIMEOUT_MS = 15e3;
|
|
85
|
+
var SEND_WAIT_MS = 4e3;
|
|
86
|
+
var SENT_HOLD_MS = 1600;
|
|
85
87
|
function parseColor(raw) {
|
|
86
88
|
const value = (raw ?? "").trim().toLowerCase();
|
|
87
89
|
if (!value || value === "transparent") return null;
|
|
@@ -511,11 +513,41 @@ h2 {
|
|
|
511
513
|
margin: 2px 0 0; font-family: var(--font-display); font-weight: var(--display-weight);
|
|
512
514
|
font-size: 22px; line-height: 1.15; letter-spacing: var(--display-tracking); text-wrap: balance;
|
|
513
515
|
}
|
|
514
|
-
.
|
|
515
|
-
.shot
|
|
516
|
-
.
|
|
517
|
-
|
|
518
|
-
|
|
516
|
+
.form { display: contents; }
|
|
517
|
+
.shot { display: flex; gap: 14px; align-items: center; font-size: 14px; }
|
|
518
|
+
.thumb {
|
|
519
|
+
position: relative; flex: none; width: 60px; aspect-ratio: var(--shot-ratio, 0.46); max-height: 124px;
|
|
520
|
+
border-radius: 9px; border: 1px solid var(--line); background: var(--fill);
|
|
521
|
+
}
|
|
522
|
+
.thumb img { display: none; width: 100%; height: 100%; object-fit: cover; object-position: top; border-radius: 8px; }
|
|
523
|
+
.scan, .empty, .badge { display: none; }
|
|
524
|
+
.shot[data-shot="taking"] .thumb { overflow: hidden; }
|
|
525
|
+
.shot[data-shot="taking"] .scan {
|
|
526
|
+
display: block; position: absolute; left: 0; right: 0; top: 0; height: 34%;
|
|
527
|
+
background: linear-gradient(to bottom, transparent, var(--line)); border-bottom: 2px solid var(--ink);
|
|
528
|
+
}
|
|
529
|
+
.shot[data-shot="attached"] .thumb { background: var(--surface); }
|
|
530
|
+
.shot[data-shot="attached"] img { display: block; }
|
|
531
|
+
.shot[data-shot="attached"] .badge {
|
|
532
|
+
display: grid; place-items: center; position: absolute; right: -8px; top: -8px; width: 22px; height: 22px;
|
|
533
|
+
border-radius: 50%; background: #1f7a4d; color: #fff; border: 2px solid var(--surface);
|
|
534
|
+
}
|
|
535
|
+
.badge svg { width: 12px; height: 12px; }
|
|
536
|
+
.shot[data-shot="none"] .thumb { border-style: dashed; background: none; }
|
|
537
|
+
.shot[data-shot="none"] .empty { display: grid; place-items: center; position: absolute; inset: 0; color: var(--ink-dim); }
|
|
538
|
+
.empty svg { width: 22px; height: 22px; }
|
|
539
|
+
.shot-text { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; min-width: 0; }
|
|
540
|
+
.shot-label { font-weight: 600; color: var(--ink); }
|
|
541
|
+
.shot[data-shot="none"] .shot-label { color: var(--ink-dim); font-weight: inherit; }
|
|
542
|
+
.finder {
|
|
543
|
+
position: fixed; z-index: 2147483002; pointer-events: none;
|
|
544
|
+
top: calc(10px + env(safe-area-inset-top, 0px)); left: 10px; right: 10px; bottom: var(--finder-bottom, 55%);
|
|
545
|
+
}
|
|
546
|
+
.finder i { position: absolute; width: 22px; height: 22px; border: 3px solid #fff; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.5)); }
|
|
547
|
+
.finder i:nth-child(1) { left: 0; top: 0; border-right: 0; border-bottom: 0; border-top-left-radius: 9px; }
|
|
548
|
+
.finder i:nth-child(2) { right: 0; top: 0; border-left: 0; border-bottom: 0; border-top-right-radius: 9px; }
|
|
549
|
+
.finder i:nth-child(3) { left: 0; bottom: 0; border-right: 0; border-top: 0; border-bottom-left-radius: 9px; }
|
|
550
|
+
.finder i:nth-child(4) { right: 0; bottom: 0; border-left: 0; border-top: 0; border-bottom-right-radius: 9px; }
|
|
519
551
|
.link { background: none; border: 0; padding: 0; font: inherit; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
|
|
520
552
|
textarea {
|
|
521
553
|
width: 100%; min-height: 84px; resize: none; font: inherit; font-size: 16px; color: var(--ink);
|
|
@@ -528,7 +560,19 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
528
560
|
cursor: pointer; border: 0; padding: 15px 14px;
|
|
529
561
|
background: var(--cta); color: var(--cta-ink); border-radius: var(--button-radius);
|
|
530
562
|
}
|
|
563
|
+
.send { display: flex; align-items: center; justify-content: center; gap: 9px; }
|
|
531
564
|
.send[disabled] { opacity: 0.5; cursor: default; }
|
|
565
|
+
.sheet[data-busy] .send { opacity: 1; }
|
|
566
|
+
.dots { display: inline-flex; gap: 4px; }
|
|
567
|
+
.dots i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; opacity: 0.4; }
|
|
568
|
+
.done { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 14px 0 10px; }
|
|
569
|
+
.ring { display: grid; place-items: center; width: 52px; height: 52px; border-radius: 50%; background: #1f7a4d; color: #fff; }
|
|
570
|
+
.ring svg { width: 26px; height: 26px; }
|
|
571
|
+
.done-title {
|
|
572
|
+
margin: 0; font-family: var(--font-display); font-weight: var(--display-weight);
|
|
573
|
+
font-size: 22px; line-height: 1.15; letter-spacing: var(--display-tracking);
|
|
574
|
+
}
|
|
575
|
+
.said { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
532
576
|
.status { margin: 0; font-size: 13px; color: var(--ink-dim); min-height: 1.4em; }
|
|
533
577
|
[hidden] { display: none !important; }
|
|
534
578
|
@media (prefers-reduced-motion: no-preference) {
|
|
@@ -538,13 +582,31 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
538
582
|
}
|
|
539
583
|
.bug svg { transition: transform 0.1s ease-out, width 0.22s ease, height 0.22s ease; }
|
|
540
584
|
.sheet { animation: rise 0.22s ease-out; }
|
|
541
|
-
.shot[data-
|
|
542
|
-
@keyframes
|
|
585
|
+
.shot[data-shot="taking"] .scan { animation: fsf-scan 1.3s cubic-bezier(0.45, 0, 0.55, 1) infinite; }
|
|
586
|
+
@keyframes fsf-scan { from { transform: translateY(-100%); } to { transform: translateY(300%); } }
|
|
587
|
+
.finder i { animation: fsf-frame 0.9s ease-in-out infinite alternate; }
|
|
588
|
+
@keyframes fsf-frame { from { opacity: 0.55; } to { opacity: 1; } }
|
|
589
|
+
.shot[data-shot="attached"] img { animation: fsf-land 0.28s cubic-bezier(0.2, 0.8, 0.2, 1); }
|
|
590
|
+
@keyframes fsf-land { from { transform: scale(1.14); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
591
|
+
.shot[data-shot="attached"] .badge { animation: fsf-pop 0.24s cubic-bezier(0.2, 0.8, 0.2, 1) 0.14s backwards; }
|
|
592
|
+
.ring { animation: fsf-pop 0.26s cubic-bezier(0.2, 0.8, 0.2, 1); }
|
|
593
|
+
@keyframes fsf-pop { from { transform: scale(0.4); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
594
|
+
.dots i { animation: fsf-dot 1s ease-in-out infinite; }
|
|
595
|
+
.dots i:nth-child(2) { animation-delay: 0.15s; }
|
|
596
|
+
.dots i:nth-child(3) { animation-delay: 0.3s; }
|
|
597
|
+
@keyframes fsf-dot { 30% { opacity: 1; transform: translateY(-2px); } 60% { opacity: 0.4; transform: none; } }
|
|
543
598
|
@keyframes rise { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
544
599
|
}
|
|
545
600
|
`;
|
|
546
601
|
var mounted = null;
|
|
547
602
|
var feedbackOptedOut = (doc = document) => doc.documentElement.getAttribute("data-fs-feedback") === "off";
|
|
603
|
+
var CHECK_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
|
|
604
|
+
var CAMERA_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="6.5" width="17" height="12.5" rx="3"/><circle cx="12" cy="12.8" r="3.2"/><path d="M9 6.5l1-2h4l1 2"/></svg>';
|
|
605
|
+
var SHOT_LABEL = {
|
|
606
|
+
taking: "Taking a screenshot",
|
|
607
|
+
attached: "Screenshot attached",
|
|
608
|
+
none: "No screenshot"
|
|
609
|
+
};
|
|
548
610
|
var SCROLL_OPTIONS = { capture: true, passive: true };
|
|
549
611
|
function mountFlowFeedback(options) {
|
|
550
612
|
if (typeof document === "undefined" || !document.body) return null;
|
|
@@ -557,14 +619,22 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
557
619
|
<button type="button" class="bug" data-state="float" aria-label="Send feedback on this page">${BUG_ICON}</button>
|
|
558
620
|
<div class="probe" aria-hidden="true"></div>
|
|
559
621
|
<div class="scrim" hidden></div>
|
|
560
|
-
<div class="
|
|
622
|
+
<div class="finder" aria-hidden="true" hidden><i></i><i></i><i></i><i></i></div>
|
|
623
|
+
<div class="sheet" role="dialog" aria-modal="true" aria-labelledby="fsf-title" data-view="form" hidden>
|
|
561
624
|
<div class="grab" aria-hidden="true"></div>
|
|
562
|
-
<
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
625
|
+
<div class="form">
|
|
626
|
+
<h2 id="fsf-title">Feedback on this page</h2>
|
|
627
|
+
<div class="shot" data-shot="none">
|
|
628
|
+
<span class="thumb"><span class="scan" aria-hidden="true"></span><img alt="Screenshot of this page" /><span class="empty" aria-hidden="true">${CAMERA_ICON}</span><span class="badge" aria-hidden="true">${CHECK_ICON}</span></span>
|
|
629
|
+
<span class="shot-text"><span class="shot-label" role="status" aria-live="polite">No screenshot</span><button type="button" class="link remove" hidden>Remove</button><button type="button" class="link retake" hidden>Take screenshot</button></span>
|
|
630
|
+
</div>
|
|
631
|
+
<textarea aria-labelledby="fsf-title" placeholder="What should change?" maxlength="2000"></textarea>
|
|
632
|
+
<p class="status" role="status" aria-live="polite"></p>
|
|
633
|
+
<button type="button" class="send" disabled><span class="send-label">Send feedback</span><span class="dots" aria-hidden="true" hidden><i></i><i></i><i></i></span></button>
|
|
634
|
+
<button type="button" class="link tuck">Tuck the button to the side</button>
|
|
635
|
+
</div>
|
|
636
|
+
<div class="done" hidden><span class="ring">${CHECK_ICON}</span><p class="done-title" aria-hidden="true">Feedback sent</p></div>
|
|
637
|
+
<p class="said" role="status" aria-live="polite"></p>
|
|
568
638
|
</div>`;
|
|
569
639
|
const q = (selector) => root.querySelector(selector);
|
|
570
640
|
const bug = q(".bug");
|
|
@@ -574,6 +644,13 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
574
644
|
const img = q(".shot img");
|
|
575
645
|
const shotLabel = q(".shot-label");
|
|
576
646
|
const remove = q(".remove");
|
|
647
|
+
const retake = q(".retake");
|
|
648
|
+
const finder = q(".finder");
|
|
649
|
+
const form = q(".form");
|
|
650
|
+
const done = q(".done");
|
|
651
|
+
const said = q(".said");
|
|
652
|
+
const sendLabel = q(".send-label");
|
|
653
|
+
const dots = q(".dots");
|
|
577
654
|
const text = q("textarea");
|
|
578
655
|
const status = q(".status");
|
|
579
656
|
const send = q(".send");
|
|
@@ -799,14 +876,54 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
799
876
|
};
|
|
800
877
|
let round = 0;
|
|
801
878
|
let pending = null;
|
|
802
|
-
const setShot = (url) => {
|
|
803
|
-
screenshot = url;
|
|
804
|
-
shot.
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
879
|
+
const setShot = (state, url = null) => {
|
|
880
|
+
screenshot = state === "attached" ? url : null;
|
|
881
|
+
shot.dataset.shot = state;
|
|
882
|
+
shotLabel.textContent = SHOT_LABEL[state];
|
|
883
|
+
remove.hidden = state !== "attached" || busy;
|
|
884
|
+
retake.hidden = state !== "none" || busy;
|
|
885
|
+
finder.hidden = state !== "taking";
|
|
886
|
+
if (screenshot) img.src = screenshot;
|
|
808
887
|
else img.removeAttribute("src");
|
|
809
888
|
};
|
|
889
|
+
const setBusy = (next) => {
|
|
890
|
+
busy = next;
|
|
891
|
+
if (busy) sheet.setAttribute("data-busy", "");
|
|
892
|
+
else sheet.removeAttribute("data-busy");
|
|
893
|
+
text.readOnly = busy;
|
|
894
|
+
sendLabel.textContent = busy ? "Sending" : "Send feedback";
|
|
895
|
+
dots.hidden = !busy;
|
|
896
|
+
const state = shot.dataset.shot;
|
|
897
|
+
remove.hidden = state !== "attached" || busy;
|
|
898
|
+
retake.hidden = state !== "none" || busy;
|
|
899
|
+
sync();
|
|
900
|
+
};
|
|
901
|
+
const setView = (view) => {
|
|
902
|
+
sheet.dataset.view = view;
|
|
903
|
+
form.hidden = view !== "form";
|
|
904
|
+
done.hidden = view !== "sent";
|
|
905
|
+
said.textContent = view === "sent" ? "Feedback sent" : "";
|
|
906
|
+
};
|
|
907
|
+
const take = () => {
|
|
908
|
+
const mine = round += 1;
|
|
909
|
+
setShot("taking");
|
|
910
|
+
host.style.setProperty("--finder-bottom", `${sheet.offsetHeight + 10}px`);
|
|
911
|
+
host.style.setProperty(
|
|
912
|
+
"--shot-ratio",
|
|
913
|
+
String(Math.max(0.3, Math.min(1, window.innerWidth / Math.max(1, window.innerHeight))))
|
|
914
|
+
);
|
|
915
|
+
const capture = options.capture ?? (() => captureViewport(theme.ground));
|
|
916
|
+
const work = withTimeout(Promise.resolve().then(capture), CAPTURE_TIMEOUT_MS).then((url) => {
|
|
917
|
+
if (mine !== round) return;
|
|
918
|
+
pending = null;
|
|
919
|
+
setShot(url ? "attached" : "none", url);
|
|
920
|
+
const active = root.activeElement;
|
|
921
|
+
if (!busy && (!active || active === bug || active === retake || active === remove))
|
|
922
|
+
text.focus();
|
|
923
|
+
});
|
|
924
|
+
pending = work;
|
|
925
|
+
return work;
|
|
926
|
+
};
|
|
810
927
|
const close = () => {
|
|
811
928
|
if (closeTimer) clearTimeout(closeTimer);
|
|
812
929
|
closeTimer = null;
|
|
@@ -815,10 +932,11 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
815
932
|
sheet.hidden = true;
|
|
816
933
|
scrim.hidden = true;
|
|
817
934
|
bug.hidden = false;
|
|
818
|
-
|
|
935
|
+
setBusy(false);
|
|
936
|
+
setShot("none");
|
|
937
|
+
setView("form");
|
|
819
938
|
text.value = "";
|
|
820
939
|
status.textContent = "";
|
|
821
|
-
busy = false;
|
|
822
940
|
sync();
|
|
823
941
|
schedulePlace();
|
|
824
942
|
};
|
|
@@ -826,34 +944,34 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
826
944
|
if (!sheet.hidden) return pending ?? Promise.resolve();
|
|
827
945
|
if (busy) return Promise.resolve();
|
|
828
946
|
paint();
|
|
829
|
-
const mine = round += 1;
|
|
830
947
|
bug.hidden = true;
|
|
831
|
-
|
|
832
|
-
shot.hidden = false;
|
|
833
|
-
shot.setAttribute("data-pending", "");
|
|
834
|
-
shotLabel.textContent = "Taking a screenshot";
|
|
948
|
+
setView("form");
|
|
835
949
|
tuck.hidden = spot.state !== "float";
|
|
836
950
|
scrim.hidden = false;
|
|
837
951
|
sheet.hidden = false;
|
|
838
952
|
sync();
|
|
839
|
-
|
|
840
|
-
pending = withTimeout(Promise.resolve().then(capture), CAPTURE_TIMEOUT_MS).then((url) => {
|
|
841
|
-
if (mine !== round) return;
|
|
842
|
-
pending = null;
|
|
843
|
-
setShot(url);
|
|
844
|
-
if (!root.activeElement || root.activeElement === bug) text.focus();
|
|
845
|
-
});
|
|
846
|
-
return pending;
|
|
953
|
+
return take();
|
|
847
954
|
};
|
|
848
955
|
const submit = async () => {
|
|
849
956
|
const message = text.value.trim();
|
|
850
957
|
if (!message || busy) return;
|
|
851
|
-
|
|
852
|
-
sync();
|
|
958
|
+
setBusy(true);
|
|
853
959
|
status.textContent = "";
|
|
854
|
-
|
|
855
|
-
if (pending)
|
|
856
|
-
|
|
960
|
+
let mine = round;
|
|
961
|
+
if (pending) {
|
|
962
|
+
let timer;
|
|
963
|
+
await Promise.race([
|
|
964
|
+
pending,
|
|
965
|
+
new Promise((resolve) => timer = setTimeout(resolve, SEND_WAIT_MS))
|
|
966
|
+
]);
|
|
967
|
+
clearTimeout(timer);
|
|
968
|
+
if (mine !== round) return;
|
|
969
|
+
if (pending) {
|
|
970
|
+
mine = round += 1;
|
|
971
|
+
pending = null;
|
|
972
|
+
setShot("none");
|
|
973
|
+
}
|
|
974
|
+
}
|
|
857
975
|
try {
|
|
858
976
|
await options.submit({
|
|
859
977
|
message,
|
|
@@ -862,15 +980,16 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
862
980
|
pagePath: window.location.pathname.slice(-200),
|
|
863
981
|
viewport: { width: window.innerWidth, height: window.innerHeight }
|
|
864
982
|
});
|
|
865
|
-
|
|
983
|
+
if (mine !== round) return;
|
|
984
|
+
setView("sent");
|
|
866
985
|
closeTimer = setTimeout(() => {
|
|
867
986
|
close();
|
|
868
987
|
bug.focus();
|
|
869
|
-
},
|
|
988
|
+
}, SENT_HOLD_MS);
|
|
870
989
|
} catch {
|
|
871
|
-
|
|
990
|
+
if (mine !== round) return;
|
|
991
|
+
setBusy(false);
|
|
872
992
|
status.textContent = "That did not send. Try again.";
|
|
873
|
-
sync();
|
|
874
993
|
}
|
|
875
994
|
};
|
|
876
995
|
bug.addEventListener("click", () => {
|
|
@@ -904,7 +1023,15 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
904
1023
|
remove.addEventListener("click", () => {
|
|
905
1024
|
round += 1;
|
|
906
1025
|
pending = null;
|
|
907
|
-
setShot(
|
|
1026
|
+
setShot("none");
|
|
1027
|
+
retake.focus();
|
|
1028
|
+
});
|
|
1029
|
+
retake.addEventListener("click", () => void take());
|
|
1030
|
+
sheet.addEventListener("click", () => {
|
|
1031
|
+
if (sheet.dataset.view === "sent") {
|
|
1032
|
+
close();
|
|
1033
|
+
bug.focus();
|
|
1034
|
+
}
|
|
908
1035
|
});
|
|
909
1036
|
text.addEventListener("input", sync);
|
|
910
1037
|
send.addEventListener("click", () => void submit());
|