@goflowstate/widget-sdk 0.3.2 → 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 +12 -2
- package/dist/canvas-widget-sdk.js +274 -39
- package/dist/canvas-widget-sdk.js.map +2 -2
- package/dist/feedback.d.ts +13 -4
- package/dist/feedback.d.ts.map +1 -1
- package/dist/index.mjs +274 -39
- package/dist/index.mjs.map +2 -2
- package/package.json +1 -1
package/dist/feedback.d.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Flow feedback: the bug icon every flow carries (flowstate-docs plan 2026-11-09-flow-feedback).
|
|
3
3
|
*
|
|
4
4
|
* The session client mounts this when the session state says `feedbackEnabled`. A round button
|
|
5
|
-
* sits in the corner of the flow's own page. A tap
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* sits in the corner of the flow's own page. A tap opens a sheet (one field, one button) and
|
|
6
|
+
* takes a screenshot of the page as the person sees it. The sheet says each step: the picture is
|
|
7
|
+
* being taken, it is attached (or there is none, with a way to take it again), the note is
|
|
8
|
+
* sending, it is sent. The note goes out through the flow's own session
|
|
9
|
+
* (`session.request('feedback', …)`).
|
|
8
10
|
*
|
|
9
11
|
* It lives in the flow's document, in a shadow root, so it takes the flow's look instead of
|
|
10
12
|
* bringing one: the flow's fonts by inheritance, and its ground, ink, main button and corner
|
|
@@ -23,6 +25,12 @@ export interface FlowFeedbackTheme {
|
|
|
23
25
|
fontBody: string;
|
|
24
26
|
fontDisplay: string;
|
|
25
27
|
displayWeight: string;
|
|
28
|
+
/** Letter spacing of the flow's titles, in em, so it scales to the sheet's title size. */
|
|
29
|
+
displayTracking: string;
|
|
30
|
+
fontButton: string;
|
|
31
|
+
buttonWeight: string;
|
|
32
|
+
buttonTracking: string;
|
|
33
|
+
buttonTransform: string;
|
|
26
34
|
radius: string;
|
|
27
35
|
buttonRadius: string;
|
|
28
36
|
dark: boolean;
|
|
@@ -41,7 +49,8 @@ export interface FlowFeedbackOptions {
|
|
|
41
49
|
/** Sends the report. The session client passes `session.request('feedback', report)`. */
|
|
42
50
|
submit: (report: FlowFeedbackReport) => Promise<unknown>;
|
|
43
51
|
/** Renders the page to a JPEG data URL, or null when it cannot. Defaults to the built-in
|
|
44
|
-
* rasterizer; tests and unusual hosts pass their own.
|
|
52
|
+
* rasterizer; tests and unusual hosts pass their own. It runs while the sheet is already
|
|
53
|
+
* open, so a host's own capture must leave the feedback host element out of the picture. */
|
|
45
54
|
capture?: () => Promise<string | null>;
|
|
46
55
|
/** Called once the icon is on the page, so a host page can hide its own fallback. */
|
|
47
56
|
onReady?: () => void;
|
package/dist/feedback.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feedback.d.ts","sourceRoot":"","sources":["../src/feedback.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"feedback.d.ts","sourceRoot":"","sources":["../src/feedback.ts"],"names":[],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -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
|
var __defProp = Object.defineProperty;
|
|
@@ -43,7 +43,9 @@ function createServiceClient(opts) {
|
|
|
43
43
|
var FEEDBACK_HOST_ATTR = "data-fs-feedback-host";
|
|
44
44
|
var SCREENSHOT_MAX_BYTES = 15e4;
|
|
45
45
|
var SCREENSHOT_MAX_SIDE = 1280;
|
|
46
|
-
var CAPTURE_TIMEOUT_MS =
|
|
46
|
+
var CAPTURE_TIMEOUT_MS = 15e3;
|
|
47
|
+
var SEND_WAIT_MS = 4e3;
|
|
48
|
+
var SENT_HOLD_MS = 1600;
|
|
47
49
|
function parseColor(raw) {
|
|
48
50
|
const value = (raw ?? "").trim().toLowerCase();
|
|
49
51
|
if (!value || value === "transparent") return null;
|
|
@@ -121,6 +123,46 @@ var primaryButton = (doc, ground) => {
|
|
|
121
123
|
});
|
|
122
124
|
return best;
|
|
123
125
|
};
|
|
126
|
+
var TEXT_SAMPLE_LIMIT = 600;
|
|
127
|
+
var DISPLAY_MIN_PX = 22;
|
|
128
|
+
var sizeOf = (style) => {
|
|
129
|
+
const n = parseFloat(style.fontSize);
|
|
130
|
+
return Number.isFinite(n) && n > 0 ? n : 16;
|
|
131
|
+
};
|
|
132
|
+
var textSamples = (doc) => {
|
|
133
|
+
const out = [];
|
|
134
|
+
const all = doc.body.querySelectorAll("*");
|
|
135
|
+
for (let i = 0; i < all.length && out.length < TEXT_SAMPLE_LIMIT; i += 1) {
|
|
136
|
+
const el = all[i];
|
|
137
|
+
if (el.tagName === "SCRIPT" || el.tagName === "STYLE" || inOurs(el)) continue;
|
|
138
|
+
let chars = 0;
|
|
139
|
+
for (const node of Array.from(el.childNodes)) {
|
|
140
|
+
if (node.nodeType === 3) chars += (node.textContent ?? "").trim().length;
|
|
141
|
+
}
|
|
142
|
+
if (chars === 0 || !visible(el)) continue;
|
|
143
|
+
const style = styleOf(el);
|
|
144
|
+
if (!style.fontFamily) continue;
|
|
145
|
+
out.push({
|
|
146
|
+
family: style.fontFamily,
|
|
147
|
+
size: sizeOf(style),
|
|
148
|
+
chars,
|
|
149
|
+
style,
|
|
150
|
+
heading: /^H[1-3]$/.test(el.tagName)
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
};
|
|
155
|
+
var trackingEm = (style, min, max, fallback) => {
|
|
156
|
+
const px = style ? parseFloat(style.letterSpacing) : NaN;
|
|
157
|
+
const em = style && Number.isFinite(px) ? px / sizeOf(style) : fallback;
|
|
158
|
+
const held = Math.min(max, Math.max(min, em));
|
|
159
|
+
return held === 0 ? "0em" : `${Math.round(held * 1e3) / 1e3}em`;
|
|
160
|
+
};
|
|
161
|
+
var weightIn = (style, min, max, fallback) => {
|
|
162
|
+
const raw = style?.fontWeight ?? "";
|
|
163
|
+
const n = raw === "bold" ? 700 : raw === "normal" ? 400 : parseFloat(raw);
|
|
164
|
+
return Number.isFinite(n) ? String(Math.min(max, Math.max(min, n))) : fallback;
|
|
165
|
+
};
|
|
124
166
|
function readFlowTheme(doc = document) {
|
|
125
167
|
const body = doc.body;
|
|
126
168
|
const stage = body.querySelector(":scope > :not(script):not(style):not([data-fs-feedback-host])");
|
|
@@ -143,12 +185,32 @@ function readFlowTheme(doc = document) {
|
|
|
143
185
|
const pinnedCtaInk = parseColor(rootVar(doc, ["--fs-feedback-cta-ink"]));
|
|
144
186
|
const sampledCtaInk = !pinnedCta && sampledCta && buttonStyle ? parseColor(buttonStyle.color) : null;
|
|
145
187
|
const ctaInk = pinnedCtaInk ?? (sampledCtaInk && contrast(sampledCtaInk, cta) >= 3 ? sampledCtaInk : null) ?? (luminance(cta) > 0.4 ? BLACK : WHITE);
|
|
146
|
-
const
|
|
188
|
+
const samples = textSamples(doc);
|
|
189
|
+
const headings = Array.from(doc.querySelectorAll('h1, h2, [class*="title"]')).filter(
|
|
147
190
|
(el) => !inOurs(el) && visible(el)
|
|
148
191
|
);
|
|
149
|
-
const
|
|
150
|
-
|
|
192
|
+
const largest = (items, size) => items.reduce(
|
|
193
|
+
(best, item) => best === null || size(item) > size(best) ? item : best,
|
|
194
|
+
null
|
|
195
|
+
);
|
|
196
|
+
const bigHeading = largest(
|
|
197
|
+
headings.filter((el) => sizeOf(styleOf(el)) >= DISPLAY_MIN_PX),
|
|
198
|
+
(el) => sizeOf(styleOf(el))
|
|
199
|
+
);
|
|
200
|
+
const bigLine = largest(
|
|
201
|
+
samples.filter((t) => t.size >= DISPLAY_MIN_PX && t.chars >= 3),
|
|
202
|
+
(t) => t.size
|
|
203
|
+
);
|
|
204
|
+
const headingStyle = bigHeading ? styleOf(bigHeading) : bigLine?.style ?? (headings[0] ? styleOf(headings[0]) : null);
|
|
205
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
206
|
+
for (const t of samples) {
|
|
207
|
+
if (t.size < DISPLAY_MIN_PX && !t.heading)
|
|
208
|
+
byFamily.set(t.family, (byFamily.get(t.family) ?? 0) + t.chars);
|
|
209
|
+
}
|
|
210
|
+
const readBody = largest(Array.from(byFamily), ([, chars]) => chars)?.[0] ?? "";
|
|
211
|
+
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";
|
|
151
212
|
const fontDisplay = rootVar(doc, ["--fs-feedback-font-display"]) || headingStyle?.fontFamily || rootVar(doc, ["--sb-font-display", "--sb-font-title", "--font-display"]) || fontBody;
|
|
213
|
+
const buttonType = buttonStyle?.fontFamily ? buttonStyle : null;
|
|
152
214
|
const field = Array.from(
|
|
153
215
|
doc.querySelectorAll('textarea, input[type="text"], input:not([type])')
|
|
154
216
|
).find((el) => !inOurs(el) && visible(el));
|
|
@@ -172,6 +234,11 @@ function readFlowTheme(doc = document) {
|
|
|
172
234
|
fontBody,
|
|
173
235
|
fontDisplay,
|
|
174
236
|
displayWeight: headingStyle?.fontWeight || "500",
|
|
237
|
+
displayTracking: trackingEm(headingStyle, -0.04, 0.02, -0.01),
|
|
238
|
+
fontButton: buttonType?.fontFamily || fontBody,
|
|
239
|
+
buttonWeight: weightIn(buttonType, 500, 800, "600"),
|
|
240
|
+
buttonTracking: trackingEm(buttonType, -0.02, 0.08, 0),
|
|
241
|
+
buttonTransform: buttonType?.textTransform === "uppercase" ? "uppercase" : "none",
|
|
175
242
|
radius,
|
|
176
243
|
buttonRadius: buttonRadius === null ? "999px" : `${Math.min(buttonRadius, 999)}px`,
|
|
177
244
|
dark
|
|
@@ -197,9 +264,10 @@ function encodeScreenshot(canvas) {
|
|
|
197
264
|
return null;
|
|
198
265
|
}
|
|
199
266
|
async function captureViewport(ground) {
|
|
200
|
-
const { domToCanvas } = await import("modern-screenshot");
|
|
201
267
|
const width = window.innerWidth;
|
|
202
268
|
const height = window.innerHeight;
|
|
269
|
+
const { scrollX, scrollY } = window;
|
|
270
|
+
const { domToCanvas } = await import("modern-screenshot");
|
|
203
271
|
const scale = Math.min(
|
|
204
272
|
window.devicePixelRatio || 1,
|
|
205
273
|
SCREENSHOT_MAX_SIDE / Math.max(width, height)
|
|
@@ -209,7 +277,7 @@ async function captureViewport(ground) {
|
|
|
209
277
|
height,
|
|
210
278
|
scale,
|
|
211
279
|
backgroundColor: ground,
|
|
212
|
-
style: { transform: `translate(${-
|
|
280
|
+
style: { transform: `translate(${-scrollX}px, ${-scrollY}px)` },
|
|
213
281
|
filter: (node) => !(node instanceof Element && node.hasAttribute(FEEDBACK_HOST_ATTR))
|
|
214
282
|
});
|
|
215
283
|
return encodeScreenshot(canvas);
|
|
@@ -403,9 +471,45 @@ var STYLE = `
|
|
|
403
471
|
font-family: var(--font-body); font-size: 15px; line-height: 1.4;
|
|
404
472
|
}
|
|
405
473
|
.grab { width: 36px; height: 4px; border-radius: 2px; background: var(--line); align-self: center; }
|
|
406
|
-
h2 {
|
|
407
|
-
|
|
408
|
-
|
|
474
|
+
h2 {
|
|
475
|
+
margin: 2px 0 0; font-family: var(--font-display); font-weight: var(--display-weight);
|
|
476
|
+
font-size: 22px; line-height: 1.15; letter-spacing: var(--display-tracking); text-wrap: balance;
|
|
477
|
+
}
|
|
478
|
+
.form { display: contents; }
|
|
479
|
+
.shot { display: flex; gap: 14px; align-items: center; font-size: 14px; }
|
|
480
|
+
.thumb {
|
|
481
|
+
position: relative; flex: none; width: 60px; aspect-ratio: var(--shot-ratio, 0.46); max-height: 124px;
|
|
482
|
+
border-radius: 9px; border: 1px solid var(--line); background: var(--fill);
|
|
483
|
+
}
|
|
484
|
+
.thumb img { display: none; width: 100%; height: 100%; object-fit: cover; object-position: top; border-radius: 8px; }
|
|
485
|
+
.scan, .empty, .badge { display: none; }
|
|
486
|
+
.shot[data-shot="taking"] .thumb { overflow: hidden; }
|
|
487
|
+
.shot[data-shot="taking"] .scan {
|
|
488
|
+
display: block; position: absolute; left: 0; right: 0; top: 0; height: 34%;
|
|
489
|
+
background: linear-gradient(to bottom, transparent, var(--line)); border-bottom: 2px solid var(--ink);
|
|
490
|
+
}
|
|
491
|
+
.shot[data-shot="attached"] .thumb { background: var(--surface); }
|
|
492
|
+
.shot[data-shot="attached"] img { display: block; }
|
|
493
|
+
.shot[data-shot="attached"] .badge {
|
|
494
|
+
display: grid; place-items: center; position: absolute; right: -8px; top: -8px; width: 22px; height: 22px;
|
|
495
|
+
border-radius: 50%; background: #1f7a4d; color: #fff; border: 2px solid var(--surface);
|
|
496
|
+
}
|
|
497
|
+
.badge svg { width: 12px; height: 12px; }
|
|
498
|
+
.shot[data-shot="none"] .thumb { border-style: dashed; background: none; }
|
|
499
|
+
.shot[data-shot="none"] .empty { display: grid; place-items: center; position: absolute; inset: 0; color: var(--ink-dim); }
|
|
500
|
+
.empty svg { width: 22px; height: 22px; }
|
|
501
|
+
.shot-text { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; min-width: 0; }
|
|
502
|
+
.shot-label { font-weight: 600; color: var(--ink); }
|
|
503
|
+
.shot[data-shot="none"] .shot-label { color: var(--ink-dim); font-weight: inherit; }
|
|
504
|
+
.finder {
|
|
505
|
+
position: fixed; z-index: 2147483002; pointer-events: none;
|
|
506
|
+
top: calc(10px + env(safe-area-inset-top, 0px)); left: 10px; right: 10px; bottom: var(--finder-bottom, 55%);
|
|
507
|
+
}
|
|
508
|
+
.finder i { position: absolute; width: 22px; height: 22px; border: 3px solid #fff; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.5)); }
|
|
509
|
+
.finder i:nth-child(1) { left: 0; top: 0; border-right: 0; border-bottom: 0; border-top-left-radius: 9px; }
|
|
510
|
+
.finder i:nth-child(2) { right: 0; top: 0; border-left: 0; border-bottom: 0; border-top-right-radius: 9px; }
|
|
511
|
+
.finder i:nth-child(3) { left: 0; bottom: 0; border-right: 0; border-top: 0; border-bottom-left-radius: 9px; }
|
|
512
|
+
.finder i:nth-child(4) { right: 0; bottom: 0; border-left: 0; border-top: 0; border-bottom-right-radius: 9px; }
|
|
409
513
|
.link { background: none; border: 0; padding: 0; font: inherit; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
|
|
410
514
|
textarea {
|
|
411
515
|
width: 100%; min-height: 84px; resize: none; font: inherit; font-size: 16px; color: var(--ink);
|
|
@@ -413,10 +517,24 @@ textarea {
|
|
|
413
517
|
}
|
|
414
518
|
textarea::placeholder { color: var(--ink-dim); }
|
|
415
519
|
.send {
|
|
416
|
-
font:
|
|
520
|
+
font-family: var(--font-button); font-weight: var(--button-weight); font-size: 17px; line-height: 1.2;
|
|
521
|
+
letter-spacing: var(--button-tracking); text-transform: var(--button-transform);
|
|
522
|
+
cursor: pointer; border: 0; padding: 15px 14px;
|
|
417
523
|
background: var(--cta); color: var(--cta-ink); border-radius: var(--button-radius);
|
|
418
524
|
}
|
|
525
|
+
.send { display: flex; align-items: center; justify-content: center; gap: 9px; }
|
|
419
526
|
.send[disabled] { opacity: 0.5; cursor: default; }
|
|
527
|
+
.sheet[data-busy] .send { opacity: 1; }
|
|
528
|
+
.dots { display: inline-flex; gap: 4px; }
|
|
529
|
+
.dots i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; opacity: 0.4; }
|
|
530
|
+
.done { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 14px 0 10px; }
|
|
531
|
+
.ring { display: grid; place-items: center; width: 52px; height: 52px; border-radius: 50%; background: #1f7a4d; color: #fff; }
|
|
532
|
+
.ring svg { width: 26px; height: 26px; }
|
|
533
|
+
.done-title {
|
|
534
|
+
margin: 0; font-family: var(--font-display); font-weight: var(--display-weight);
|
|
535
|
+
font-size: 22px; line-height: 1.15; letter-spacing: var(--display-tracking);
|
|
536
|
+
}
|
|
537
|
+
.said { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
420
538
|
.status { margin: 0; font-size: 13px; color: var(--ink-dim); min-height: 1.4em; }
|
|
421
539
|
[hidden] { display: none !important; }
|
|
422
540
|
@media (prefers-reduced-motion: no-preference) {
|
|
@@ -426,11 +544,31 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
426
544
|
}
|
|
427
545
|
.bug svg { transition: transform 0.1s ease-out, width 0.22s ease, height 0.22s ease; }
|
|
428
546
|
.sheet { animation: rise 0.22s ease-out; }
|
|
547
|
+
.shot[data-shot="taking"] .scan { animation: fsf-scan 1.3s cubic-bezier(0.45, 0, 0.55, 1) infinite; }
|
|
548
|
+
@keyframes fsf-scan { from { transform: translateY(-100%); } to { transform: translateY(300%); } }
|
|
549
|
+
.finder i { animation: fsf-frame 0.9s ease-in-out infinite alternate; }
|
|
550
|
+
@keyframes fsf-frame { from { opacity: 0.55; } to { opacity: 1; } }
|
|
551
|
+
.shot[data-shot="attached"] img { animation: fsf-land 0.28s cubic-bezier(0.2, 0.8, 0.2, 1); }
|
|
552
|
+
@keyframes fsf-land { from { transform: scale(1.14); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
553
|
+
.shot[data-shot="attached"] .badge { animation: fsf-pop 0.24s cubic-bezier(0.2, 0.8, 0.2, 1) 0.14s backwards; }
|
|
554
|
+
.ring { animation: fsf-pop 0.26s cubic-bezier(0.2, 0.8, 0.2, 1); }
|
|
555
|
+
@keyframes fsf-pop { from { transform: scale(0.4); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
556
|
+
.dots i { animation: fsf-dot 1s ease-in-out infinite; }
|
|
557
|
+
.dots i:nth-child(2) { animation-delay: 0.15s; }
|
|
558
|
+
.dots i:nth-child(3) { animation-delay: 0.3s; }
|
|
559
|
+
@keyframes fsf-dot { 30% { opacity: 1; transform: translateY(-2px); } 60% { opacity: 0.4; transform: none; } }
|
|
429
560
|
@keyframes rise { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
430
561
|
}
|
|
431
562
|
`;
|
|
432
563
|
var mounted = null;
|
|
433
564
|
var feedbackOptedOut = (doc = document) => doc.documentElement.getAttribute("data-fs-feedback") === "off";
|
|
565
|
+
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>';
|
|
566
|
+
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>';
|
|
567
|
+
var SHOT_LABEL = {
|
|
568
|
+
taking: "Taking a screenshot",
|
|
569
|
+
attached: "Screenshot attached",
|
|
570
|
+
none: "No screenshot"
|
|
571
|
+
};
|
|
434
572
|
var SCROLL_OPTIONS = { capture: true, passive: true };
|
|
435
573
|
function mountFlowFeedback(options) {
|
|
436
574
|
if (typeof document === "undefined" || !document.body) return null;
|
|
@@ -443,14 +581,22 @@ function mountFlowFeedback(options) {
|
|
|
443
581
|
<button type="button" class="bug" data-state="float" aria-label="Send feedback on this page">${BUG_ICON}</button>
|
|
444
582
|
<div class="probe" aria-hidden="true"></div>
|
|
445
583
|
<div class="scrim" hidden></div>
|
|
446
|
-
<div class="
|
|
584
|
+
<div class="finder" aria-hidden="true" hidden><i></i><i></i><i></i><i></i></div>
|
|
585
|
+
<div class="sheet" role="dialog" aria-modal="true" aria-labelledby="fsf-title" data-view="form" hidden>
|
|
447
586
|
<div class="grab" aria-hidden="true"></div>
|
|
448
|
-
<
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
587
|
+
<div class="form">
|
|
588
|
+
<h2 id="fsf-title">Feedback on this page</h2>
|
|
589
|
+
<div class="shot" data-shot="none">
|
|
590
|
+
<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>
|
|
591
|
+
<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>
|
|
592
|
+
</div>
|
|
593
|
+
<textarea aria-labelledby="fsf-title" placeholder="What should change?" maxlength="2000"></textarea>
|
|
594
|
+
<p class="status" role="status" aria-live="polite"></p>
|
|
595
|
+
<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>
|
|
596
|
+
<button type="button" class="link tuck">Tuck the button to the side</button>
|
|
597
|
+
</div>
|
|
598
|
+
<div class="done" hidden><span class="ring">${CHECK_ICON}</span><p class="done-title" aria-hidden="true">Feedback sent</p></div>
|
|
599
|
+
<p class="said" role="status" aria-live="polite"></p>
|
|
454
600
|
</div>`;
|
|
455
601
|
const q = (selector) => root.querySelector(selector);
|
|
456
602
|
const bug = q(".bug");
|
|
@@ -458,7 +604,15 @@ function mountFlowFeedback(options) {
|
|
|
458
604
|
const sheet = q(".sheet");
|
|
459
605
|
const shot = q(".shot");
|
|
460
606
|
const img = q(".shot img");
|
|
607
|
+
const shotLabel = q(".shot-label");
|
|
461
608
|
const remove = q(".remove");
|
|
609
|
+
const retake = q(".retake");
|
|
610
|
+
const finder = q(".finder");
|
|
611
|
+
const form = q(".form");
|
|
612
|
+
const done = q(".done");
|
|
613
|
+
const said = q(".said");
|
|
614
|
+
const sendLabel = q(".send-label");
|
|
615
|
+
const dots = q(".dots");
|
|
462
616
|
const text = q("textarea");
|
|
463
617
|
const status = q(".status");
|
|
464
618
|
const send = q(".send");
|
|
@@ -668,6 +822,11 @@ function mountFlowFeedback(options) {
|
|
|
668
822
|
"--font-body": theme.fontBody,
|
|
669
823
|
"--font-display": theme.fontDisplay,
|
|
670
824
|
"--display-weight": theme.displayWeight,
|
|
825
|
+
"--display-tracking": theme.displayTracking,
|
|
826
|
+
"--font-button": theme.fontButton,
|
|
827
|
+
"--button-weight": theme.buttonWeight,
|
|
828
|
+
"--button-tracking": theme.buttonTracking,
|
|
829
|
+
"--button-transform": theme.buttonTransform,
|
|
671
830
|
"--radius": theme.radius,
|
|
672
831
|
"--button-radius": theme.buttonRadius
|
|
673
832
|
};
|
|
@@ -677,46 +836,104 @@ function mountFlowFeedback(options) {
|
|
|
677
836
|
const sync = () => {
|
|
678
837
|
send.disabled = busy || text.value.trim().length === 0;
|
|
679
838
|
};
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
839
|
+
let round = 0;
|
|
840
|
+
let pending = null;
|
|
841
|
+
const setShot = (state, url = null) => {
|
|
842
|
+
screenshot = state === "attached" ? url : null;
|
|
843
|
+
shot.dataset.shot = state;
|
|
844
|
+
shotLabel.textContent = SHOT_LABEL[state];
|
|
845
|
+
remove.hidden = state !== "attached" || busy;
|
|
846
|
+
retake.hidden = state !== "none" || busy;
|
|
847
|
+
finder.hidden = state !== "taking";
|
|
848
|
+
if (screenshot) img.src = screenshot;
|
|
684
849
|
else img.removeAttribute("src");
|
|
685
850
|
};
|
|
851
|
+
const setBusy = (next) => {
|
|
852
|
+
busy = next;
|
|
853
|
+
if (busy) sheet.setAttribute("data-busy", "");
|
|
854
|
+
else sheet.removeAttribute("data-busy");
|
|
855
|
+
text.readOnly = busy;
|
|
856
|
+
sendLabel.textContent = busy ? "Sending" : "Send feedback";
|
|
857
|
+
dots.hidden = !busy;
|
|
858
|
+
const state = shot.dataset.shot;
|
|
859
|
+
remove.hidden = state !== "attached" || busy;
|
|
860
|
+
retake.hidden = state !== "none" || busy;
|
|
861
|
+
sync();
|
|
862
|
+
};
|
|
863
|
+
const setView = (view) => {
|
|
864
|
+
sheet.dataset.view = view;
|
|
865
|
+
form.hidden = view !== "form";
|
|
866
|
+
done.hidden = view !== "sent";
|
|
867
|
+
said.textContent = view === "sent" ? "Feedback sent" : "";
|
|
868
|
+
};
|
|
869
|
+
const take = () => {
|
|
870
|
+
const mine = round += 1;
|
|
871
|
+
setShot("taking");
|
|
872
|
+
host.style.setProperty("--finder-bottom", `${sheet.offsetHeight + 10}px`);
|
|
873
|
+
host.style.setProperty(
|
|
874
|
+
"--shot-ratio",
|
|
875
|
+
String(Math.max(0.3, Math.min(1, window.innerWidth / Math.max(1, window.innerHeight))))
|
|
876
|
+
);
|
|
877
|
+
const capture = options.capture ?? (() => captureViewport(theme.ground));
|
|
878
|
+
const work = withTimeout(Promise.resolve().then(capture), CAPTURE_TIMEOUT_MS).then((url) => {
|
|
879
|
+
if (mine !== round) return;
|
|
880
|
+
pending = null;
|
|
881
|
+
setShot(url ? "attached" : "none", url);
|
|
882
|
+
const active = root.activeElement;
|
|
883
|
+
if (!busy && (!active || active === bug || active === retake || active === remove))
|
|
884
|
+
text.focus();
|
|
885
|
+
});
|
|
886
|
+
pending = work;
|
|
887
|
+
return work;
|
|
888
|
+
};
|
|
686
889
|
const close = () => {
|
|
687
890
|
if (closeTimer) clearTimeout(closeTimer);
|
|
688
891
|
closeTimer = null;
|
|
892
|
+
round += 1;
|
|
893
|
+
pending = null;
|
|
689
894
|
sheet.hidden = true;
|
|
690
895
|
scrim.hidden = true;
|
|
691
896
|
bug.hidden = false;
|
|
692
|
-
|
|
897
|
+
setBusy(false);
|
|
898
|
+
setShot("none");
|
|
899
|
+
setView("form");
|
|
693
900
|
text.value = "";
|
|
694
901
|
status.textContent = "";
|
|
695
|
-
busy = false;
|
|
696
902
|
sync();
|
|
697
903
|
schedulePlace();
|
|
698
904
|
};
|
|
699
|
-
const open =
|
|
700
|
-
if (!sheet.hidden
|
|
701
|
-
busy
|
|
905
|
+
const open = () => {
|
|
906
|
+
if (!sheet.hidden) return pending ?? Promise.resolve();
|
|
907
|
+
if (busy) return Promise.resolve();
|
|
702
908
|
paint();
|
|
703
909
|
bug.hidden = true;
|
|
704
|
-
|
|
705
|
-
const url = await withTimeout(Promise.resolve().then(capture), CAPTURE_TIMEOUT_MS);
|
|
706
|
-
busy = false;
|
|
707
|
-
setShot(url);
|
|
910
|
+
setView("form");
|
|
708
911
|
tuck.hidden = spot.state !== "float";
|
|
709
912
|
scrim.hidden = false;
|
|
710
913
|
sheet.hidden = false;
|
|
711
914
|
sync();
|
|
712
|
-
|
|
915
|
+
return take();
|
|
713
916
|
};
|
|
714
917
|
const submit = async () => {
|
|
715
918
|
const message = text.value.trim();
|
|
716
919
|
if (!message || busy) return;
|
|
717
|
-
|
|
718
|
-
sync();
|
|
920
|
+
setBusy(true);
|
|
719
921
|
status.textContent = "";
|
|
922
|
+
let mine = round;
|
|
923
|
+
if (pending) {
|
|
924
|
+
let timer;
|
|
925
|
+
await Promise.race([
|
|
926
|
+
pending,
|
|
927
|
+
new Promise((resolve) => timer = setTimeout(resolve, SEND_WAIT_MS))
|
|
928
|
+
]);
|
|
929
|
+
clearTimeout(timer);
|
|
930
|
+
if (mine !== round) return;
|
|
931
|
+
if (pending) {
|
|
932
|
+
mine = round += 1;
|
|
933
|
+
pending = null;
|
|
934
|
+
setShot("none");
|
|
935
|
+
}
|
|
936
|
+
}
|
|
720
937
|
try {
|
|
721
938
|
await options.submit({
|
|
722
939
|
message,
|
|
@@ -725,21 +942,27 @@ function mountFlowFeedback(options) {
|
|
|
725
942
|
pagePath: window.location.pathname.slice(-200),
|
|
726
943
|
viewport: { width: window.innerWidth, height: window.innerHeight }
|
|
727
944
|
});
|
|
728
|
-
|
|
945
|
+
if (mine !== round) return;
|
|
946
|
+
setView("sent");
|
|
729
947
|
closeTimer = setTimeout(() => {
|
|
730
948
|
close();
|
|
731
949
|
bug.focus();
|
|
732
|
-
},
|
|
950
|
+
}, SENT_HOLD_MS);
|
|
733
951
|
} catch {
|
|
734
|
-
|
|
952
|
+
if (mine !== round) return;
|
|
953
|
+
setBusy(false);
|
|
735
954
|
status.textContent = "That did not send. Try again.";
|
|
736
|
-
sync();
|
|
737
955
|
}
|
|
738
956
|
};
|
|
739
957
|
bug.addEventListener("click", () => {
|
|
740
958
|
if (!suppressClick) void open();
|
|
741
959
|
});
|
|
742
960
|
bug.addEventListener("pointerdown", onDown);
|
|
961
|
+
if (!options.capture)
|
|
962
|
+
bug.addEventListener("pointerdown", () => void import("modern-screenshot").catch(() => {
|
|
963
|
+
}), {
|
|
964
|
+
once: true
|
|
965
|
+
});
|
|
743
966
|
bug.addEventListener("pointermove", onMove);
|
|
744
967
|
bug.addEventListener("pointerup", onUp);
|
|
745
968
|
bug.addEventListener("pointercancel", onUp);
|
|
@@ -759,7 +982,19 @@ function mountFlowFeedback(options) {
|
|
|
759
982
|
);
|
|
760
983
|
});
|
|
761
984
|
scrim.addEventListener("click", close);
|
|
762
|
-
remove.addEventListener("click", () =>
|
|
985
|
+
remove.addEventListener("click", () => {
|
|
986
|
+
round += 1;
|
|
987
|
+
pending = null;
|
|
988
|
+
setShot("none");
|
|
989
|
+
retake.focus();
|
|
990
|
+
});
|
|
991
|
+
retake.addEventListener("click", () => void take());
|
|
992
|
+
sheet.addEventListener("click", () => {
|
|
993
|
+
if (sheet.dataset.view === "sent") {
|
|
994
|
+
close();
|
|
995
|
+
bug.focus();
|
|
996
|
+
}
|
|
997
|
+
});
|
|
763
998
|
text.addEventListener("input", sync);
|
|
764
999
|
send.addEventListener("click", () => void submit());
|
|
765
1000
|
sheet.addEventListener("keydown", (event) => {
|