@goflowstate/widget-sdk 0.3.2 → 0.3.4
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 +9 -2
- package/dist/canvas-widget-sdk.js +126 -18
- package/dist/canvas-widget-sdk.js.map +2 -2
- package/dist/feedback.d.ts +8 -1
- package/dist/feedback.d.ts.map +1 -1
- package/dist/index.mjs +126 -18
- package/dist/index.mjs.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,8 +160,9 @@ before the first state arrives.
|
|
|
160
160
|
|
|
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
|
-
button in the corner of the page. A tap
|
|
164
|
-
|
|
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; the sheet shows "Taking a screenshot" until the picture is
|
|
165
|
+
attached, and neither the icon nor the sheet is in it. The note is sent through the flow's session to `POST /api/w/:slug/feedback`.
|
|
165
166
|
|
|
166
167
|
The icon and the sheet take the look of the page they sit on: its fonts, ground, ink, the fill
|
|
167
168
|
and shape of its main button, and its corner radius. To pin a role, set a custom property on
|
|
@@ -174,6 +175,12 @@ and shape of its main button, and its corner radius. To pin a role, set a custom
|
|
|
174
175
|
}
|
|
175
176
|
```
|
|
176
177
|
|
|
178
|
+
The sheet's type comes from the words the flow is drawing, not from `<body>`: the title takes
|
|
179
|
+
the face, weight and letter spacing of the flow's largest heading, the note and the field take
|
|
180
|
+
the face most of the flow's smaller text is set in, and the send button takes the face, weight,
|
|
181
|
+
letter spacing and case of the flow's main button. Pin `--fs-feedback-font` or
|
|
182
|
+
`--fs-feedback-font-display` when the reading is off.
|
|
183
|
+
|
|
177
184
|
The button floats over the flow and belongs to the person using it. They can drag it anywhere,
|
|
178
185
|
and it settles on the nearer side. A flick at an edge, or a drag into one, docks it there as a
|
|
179
186
|
slim tab with the bug still showing; a tap on the tab opens the sheet, and pulling the tab
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @goflowstate/widget-sdk v0.3.
|
|
1
|
+
// @goflowstate/widget-sdk v0.3.4
|
|
2
2
|
// https://github.com/goflowstate/goflowstate-sdk
|
|
3
3
|
|
|
4
4
|
"use strict";
|
|
@@ -159,6 +159,46 @@ var CanvasWidgetSDK = (() => {
|
|
|
159
159
|
});
|
|
160
160
|
return best;
|
|
161
161
|
};
|
|
162
|
+
var TEXT_SAMPLE_LIMIT = 600;
|
|
163
|
+
var DISPLAY_MIN_PX = 22;
|
|
164
|
+
var sizeOf = (style) => {
|
|
165
|
+
const n = parseFloat(style.fontSize);
|
|
166
|
+
return Number.isFinite(n) && n > 0 ? n : 16;
|
|
167
|
+
};
|
|
168
|
+
var textSamples = (doc) => {
|
|
169
|
+
const out = [];
|
|
170
|
+
const all = doc.body.querySelectorAll("*");
|
|
171
|
+
for (let i = 0; i < all.length && out.length < TEXT_SAMPLE_LIMIT; i += 1) {
|
|
172
|
+
const el = all[i];
|
|
173
|
+
if (el.tagName === "SCRIPT" || el.tagName === "STYLE" || inOurs(el)) continue;
|
|
174
|
+
let chars = 0;
|
|
175
|
+
for (const node of Array.from(el.childNodes)) {
|
|
176
|
+
if (node.nodeType === 3) chars += (node.textContent ?? "").trim().length;
|
|
177
|
+
}
|
|
178
|
+
if (chars === 0 || !visible(el)) continue;
|
|
179
|
+
const style = styleOf(el);
|
|
180
|
+
if (!style.fontFamily) continue;
|
|
181
|
+
out.push({
|
|
182
|
+
family: style.fontFamily,
|
|
183
|
+
size: sizeOf(style),
|
|
184
|
+
chars,
|
|
185
|
+
style,
|
|
186
|
+
heading: /^H[1-3]$/.test(el.tagName)
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
};
|
|
191
|
+
var trackingEm = (style, min, max, fallback) => {
|
|
192
|
+
const px = style ? parseFloat(style.letterSpacing) : NaN;
|
|
193
|
+
const em = style && Number.isFinite(px) ? px / sizeOf(style) : fallback;
|
|
194
|
+
const held = Math.min(max, Math.max(min, em));
|
|
195
|
+
return held === 0 ? "0em" : `${Math.round(held * 1e3) / 1e3}em`;
|
|
196
|
+
};
|
|
197
|
+
var weightIn = (style, min, max, fallback) => {
|
|
198
|
+
const raw = style?.fontWeight ?? "";
|
|
199
|
+
const n = raw === "bold" ? 700 : raw === "normal" ? 400 : parseFloat(raw);
|
|
200
|
+
return Number.isFinite(n) ? String(Math.min(max, Math.max(min, n))) : fallback;
|
|
201
|
+
};
|
|
162
202
|
function readFlowTheme(doc = document) {
|
|
163
203
|
const body = doc.body;
|
|
164
204
|
const stage = body.querySelector(":scope > :not(script):not(style):not([data-fs-feedback-host])");
|
|
@@ -181,12 +221,32 @@ var CanvasWidgetSDK = (() => {
|
|
|
181
221
|
const pinnedCtaInk = parseColor(rootVar(doc, ["--fs-feedback-cta-ink"]));
|
|
182
222
|
const sampledCtaInk = !pinnedCta && sampledCta && buttonStyle ? parseColor(buttonStyle.color) : null;
|
|
183
223
|
const ctaInk = pinnedCtaInk ?? (sampledCtaInk && contrast(sampledCtaInk, cta) >= 3 ? sampledCtaInk : null) ?? (luminance(cta) > 0.4 ? BLACK : WHITE);
|
|
184
|
-
const
|
|
224
|
+
const samples = textSamples(doc);
|
|
225
|
+
const headings = Array.from(doc.querySelectorAll('h1, h2, [class*="title"]')).filter(
|
|
185
226
|
(el) => !inOurs(el) && visible(el)
|
|
186
227
|
);
|
|
187
|
-
const
|
|
188
|
-
|
|
228
|
+
const largest = (items, size) => items.reduce(
|
|
229
|
+
(best, item) => best === null || size(item) > size(best) ? item : best,
|
|
230
|
+
null
|
|
231
|
+
);
|
|
232
|
+
const bigHeading = largest(
|
|
233
|
+
headings.filter((el) => sizeOf(styleOf(el)) >= DISPLAY_MIN_PX),
|
|
234
|
+
(el) => sizeOf(styleOf(el))
|
|
235
|
+
);
|
|
236
|
+
const bigLine = largest(
|
|
237
|
+
samples.filter((t) => t.size >= DISPLAY_MIN_PX && t.chars >= 3),
|
|
238
|
+
(t) => t.size
|
|
239
|
+
);
|
|
240
|
+
const headingStyle = bigHeading ? styleOf(bigHeading) : bigLine?.style ?? (headings[0] ? styleOf(headings[0]) : null);
|
|
241
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
242
|
+
for (const t of samples) {
|
|
243
|
+
if (t.size < DISPLAY_MIN_PX && !t.heading)
|
|
244
|
+
byFamily.set(t.family, (byFamily.get(t.family) ?? 0) + t.chars);
|
|
245
|
+
}
|
|
246
|
+
const readBody = largest(Array.from(byFamily), ([, chars]) => chars)?.[0] ?? "";
|
|
247
|
+
const fontBody = rootVar(doc, ["--fs-feedback-font"]) || readBody || (stage ? styleOf(stage).fontFamily : "") || bodyStyle.fontFamily || rootVar(doc, ["--sb-font-body", "--font-body"]) || "system-ui, sans-serif";
|
|
189
248
|
const fontDisplay = rootVar(doc, ["--fs-feedback-font-display"]) || headingStyle?.fontFamily || rootVar(doc, ["--sb-font-display", "--sb-font-title", "--font-display"]) || fontBody;
|
|
249
|
+
const buttonType = buttonStyle?.fontFamily ? buttonStyle : null;
|
|
190
250
|
const field = Array.from(
|
|
191
251
|
doc.querySelectorAll('textarea, input[type="text"], input:not([type])')
|
|
192
252
|
).find((el) => !inOurs(el) && visible(el));
|
|
@@ -210,6 +270,11 @@ var CanvasWidgetSDK = (() => {
|
|
|
210
270
|
fontBody,
|
|
211
271
|
fontDisplay,
|
|
212
272
|
displayWeight: headingStyle?.fontWeight || "500",
|
|
273
|
+
displayTracking: trackingEm(headingStyle, -0.04, 0.02, -0.01),
|
|
274
|
+
fontButton: buttonType?.fontFamily || fontBody,
|
|
275
|
+
buttonWeight: weightIn(buttonType, 500, 800, "600"),
|
|
276
|
+
buttonTracking: trackingEm(buttonType, -0.02, 0.08, 0),
|
|
277
|
+
buttonTransform: buttonType?.textTransform === "uppercase" ? "uppercase" : "none",
|
|
213
278
|
radius,
|
|
214
279
|
buttonRadius: buttonRadius === null ? "999px" : `${Math.min(buttonRadius, 999)}px`,
|
|
215
280
|
dark
|
|
@@ -235,9 +300,10 @@ var CanvasWidgetSDK = (() => {
|
|
|
235
300
|
return null;
|
|
236
301
|
}
|
|
237
302
|
async function captureViewport(ground) {
|
|
238
|
-
const { domToCanvas: domToCanvas2 } = await Promise.resolve().then(() => (init_modern_screenshot(), modern_screenshot_exports));
|
|
239
303
|
const width = window.innerWidth;
|
|
240
304
|
const height = window.innerHeight;
|
|
305
|
+
const { scrollX, scrollY } = window;
|
|
306
|
+
const { domToCanvas: domToCanvas2 } = await Promise.resolve().then(() => (init_modern_screenshot(), modern_screenshot_exports));
|
|
241
307
|
const scale = Math.min(
|
|
242
308
|
window.devicePixelRatio || 1,
|
|
243
309
|
SCREENSHOT_MAX_SIDE / Math.max(width, height)
|
|
@@ -247,7 +313,7 @@ var CanvasWidgetSDK = (() => {
|
|
|
247
313
|
height,
|
|
248
314
|
scale,
|
|
249
315
|
backgroundColor: ground,
|
|
250
|
-
style: { transform: `translate(${-
|
|
316
|
+
style: { transform: `translate(${-scrollX}px, ${-scrollY}px)` },
|
|
251
317
|
filter: (node) => !(node instanceof Element && node.hasAttribute(FEEDBACK_HOST_ATTR))
|
|
252
318
|
});
|
|
253
319
|
return encodeScreenshot(canvas);
|
|
@@ -441,9 +507,15 @@ var CanvasWidgetSDK = (() => {
|
|
|
441
507
|
font-family: var(--font-body); font-size: 15px; line-height: 1.4;
|
|
442
508
|
}
|
|
443
509
|
.grab { width: 36px; height: 4px; border-radius: 2px; background: var(--line); align-self: center; }
|
|
444
|
-
h2 {
|
|
510
|
+
h2 {
|
|
511
|
+
margin: 2px 0 0; font-family: var(--font-display); font-weight: var(--display-weight);
|
|
512
|
+
font-size: 22px; line-height: 1.15; letter-spacing: var(--display-tracking); text-wrap: balance;
|
|
513
|
+
}
|
|
445
514
|
.shot { display: flex; gap: 12px; align-items: center; font-size: 13px; color: var(--ink-dim); }
|
|
446
515
|
.shot img { width: 50px; max-height: 108px; object-fit: cover; object-position: top; border-radius: 8px; border: 1px solid var(--line); }
|
|
516
|
+
.ghost { display: none; width: 50px; height: 108px; border-radius: 8px; border: 1px solid var(--line); background: var(--fill); }
|
|
517
|
+
.shot[data-pending] .ghost { display: block; }
|
|
518
|
+
.shot[data-pending] img { display: none; }
|
|
447
519
|
.link { background: none; border: 0; padding: 0; font: inherit; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
|
|
448
520
|
textarea {
|
|
449
521
|
width: 100%; min-height: 84px; resize: none; font: inherit; font-size: 16px; color: var(--ink);
|
|
@@ -451,7 +523,9 @@ textarea {
|
|
|
451
523
|
}
|
|
452
524
|
textarea::placeholder { color: var(--ink-dim); }
|
|
453
525
|
.send {
|
|
454
|
-
font:
|
|
526
|
+
font-family: var(--font-button); font-weight: var(--button-weight); font-size: 17px; line-height: 1.2;
|
|
527
|
+
letter-spacing: var(--button-tracking); text-transform: var(--button-transform);
|
|
528
|
+
cursor: pointer; border: 0; padding: 15px 14px;
|
|
455
529
|
background: var(--cta); color: var(--cta-ink); border-radius: var(--button-radius);
|
|
456
530
|
}
|
|
457
531
|
.send[disabled] { opacity: 0.5; cursor: default; }
|
|
@@ -464,6 +538,8 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
464
538
|
}
|
|
465
539
|
.bug svg { transition: transform 0.1s ease-out, width 0.22s ease, height 0.22s ease; }
|
|
466
540
|
.sheet { animation: rise 0.22s ease-out; }
|
|
541
|
+
.shot[data-pending] .ghost { animation: wait 1.1s ease-in-out infinite alternate; }
|
|
542
|
+
@keyframes wait { from { opacity: 0.45; } to { opacity: 1; } }
|
|
467
543
|
@keyframes rise { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
468
544
|
}
|
|
469
545
|
`;
|
|
@@ -484,7 +560,7 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
484
560
|
<div class="sheet" role="dialog" aria-modal="true" aria-labelledby="fsf-title" hidden>
|
|
485
561
|
<div class="grab" aria-hidden="true"></div>
|
|
486
562
|
<h2 id="fsf-title">Feedback on this page</h2>
|
|
487
|
-
<div class="shot" hidden><img alt="Screenshot of this page" /><span><span>Screenshot attached</span><br /><button type="button" class="link remove">Remove screenshot</button></span></div>
|
|
563
|
+
<div class="shot" hidden><span class="ghost" aria-hidden="true"></span><img alt="Screenshot of this page" /><span><span class="shot-label">Screenshot attached</span><br /><button type="button" class="link remove">Remove screenshot</button></span></div>
|
|
488
564
|
<textarea aria-labelledby="fsf-title" placeholder="What should change?" maxlength="2000"></textarea>
|
|
489
565
|
<p class="status" role="status" aria-live="polite"></p>
|
|
490
566
|
<button type="button" class="send" disabled>Send feedback</button>
|
|
@@ -496,6 +572,7 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
496
572
|
const sheet = q(".sheet");
|
|
497
573
|
const shot = q(".shot");
|
|
498
574
|
const img = q(".shot img");
|
|
575
|
+
const shotLabel = q(".shot-label");
|
|
499
576
|
const remove = q(".remove");
|
|
500
577
|
const text = q("textarea");
|
|
501
578
|
const status = q(".status");
|
|
@@ -706,6 +783,11 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
706
783
|
"--font-body": theme.fontBody,
|
|
707
784
|
"--font-display": theme.fontDisplay,
|
|
708
785
|
"--display-weight": theme.displayWeight,
|
|
786
|
+
"--display-tracking": theme.displayTracking,
|
|
787
|
+
"--font-button": theme.fontButton,
|
|
788
|
+
"--button-weight": theme.buttonWeight,
|
|
789
|
+
"--button-tracking": theme.buttonTracking,
|
|
790
|
+
"--button-transform": theme.buttonTransform,
|
|
709
791
|
"--radius": theme.radius,
|
|
710
792
|
"--button-radius": theme.buttonRadius
|
|
711
793
|
};
|
|
@@ -715,15 +797,21 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
715
797
|
const sync = () => {
|
|
716
798
|
send.disabled = busy || text.value.trim().length === 0;
|
|
717
799
|
};
|
|
800
|
+
let round = 0;
|
|
801
|
+
let pending = null;
|
|
718
802
|
const setShot = (url) => {
|
|
719
803
|
screenshot = url;
|
|
720
804
|
shot.hidden = url === null;
|
|
805
|
+
shot.removeAttribute("data-pending");
|
|
806
|
+
shotLabel.textContent = "Screenshot attached";
|
|
721
807
|
if (url) img.src = url;
|
|
722
808
|
else img.removeAttribute("src");
|
|
723
809
|
};
|
|
724
810
|
const close = () => {
|
|
725
811
|
if (closeTimer) clearTimeout(closeTimer);
|
|
726
812
|
closeTimer = null;
|
|
813
|
+
round += 1;
|
|
814
|
+
pending = null;
|
|
727
815
|
sheet.hidden = true;
|
|
728
816
|
scrim.hidden = true;
|
|
729
817
|
bug.hidden = false;
|
|
@@ -734,20 +822,28 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
734
822
|
sync();
|
|
735
823
|
schedulePlace();
|
|
736
824
|
};
|
|
737
|
-
const open =
|
|
738
|
-
if (!sheet.hidden
|
|
739
|
-
busy
|
|
825
|
+
const open = () => {
|
|
826
|
+
if (!sheet.hidden) return pending ?? Promise.resolve();
|
|
827
|
+
if (busy) return Promise.resolve();
|
|
740
828
|
paint();
|
|
829
|
+
const mine = round += 1;
|
|
741
830
|
bug.hidden = true;
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
831
|
+
setShot(null);
|
|
832
|
+
shot.hidden = false;
|
|
833
|
+
shot.setAttribute("data-pending", "");
|
|
834
|
+
shotLabel.textContent = "Taking a screenshot";
|
|
746
835
|
tuck.hidden = spot.state !== "float";
|
|
747
836
|
scrim.hidden = false;
|
|
748
837
|
sheet.hidden = false;
|
|
749
838
|
sync();
|
|
750
|
-
|
|
839
|
+
const capture = options.capture ?? (() => captureViewport(theme.ground));
|
|
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;
|
|
751
847
|
};
|
|
752
848
|
const submit = async () => {
|
|
753
849
|
const message = text.value.trim();
|
|
@@ -755,6 +851,9 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
755
851
|
busy = true;
|
|
756
852
|
sync();
|
|
757
853
|
status.textContent = "";
|
|
854
|
+
const mine = round;
|
|
855
|
+
if (pending) await pending;
|
|
856
|
+
if (mine !== round) return;
|
|
758
857
|
try {
|
|
759
858
|
await options.submit({
|
|
760
859
|
message,
|
|
@@ -778,6 +877,11 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
778
877
|
if (!suppressClick) void open();
|
|
779
878
|
});
|
|
780
879
|
bug.addEventListener("pointerdown", onDown);
|
|
880
|
+
if (!options.capture)
|
|
881
|
+
bug.addEventListener("pointerdown", () => void Promise.resolve().then(() => (init_modern_screenshot(), modern_screenshot_exports)).catch(() => {
|
|
882
|
+
}), {
|
|
883
|
+
once: true
|
|
884
|
+
});
|
|
781
885
|
bug.addEventListener("pointermove", onMove);
|
|
782
886
|
bug.addEventListener("pointerup", onUp);
|
|
783
887
|
bug.addEventListener("pointercancel", onUp);
|
|
@@ -797,7 +901,11 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
797
901
|
);
|
|
798
902
|
});
|
|
799
903
|
scrim.addEventListener("click", close);
|
|
800
|
-
remove.addEventListener("click", () =>
|
|
904
|
+
remove.addEventListener("click", () => {
|
|
905
|
+
round += 1;
|
|
906
|
+
pending = null;
|
|
907
|
+
setShot(null);
|
|
908
|
+
});
|
|
801
909
|
text.addEventListener("input", sync);
|
|
802
910
|
send.addEventListener("click", () => void submit());
|
|
803
911
|
sheet.addEventListener("keydown", (event) => {
|