@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/README.md
CHANGED
|
@@ -160,8 +160,12 @@ 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, 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`.
|
|
165
169
|
|
|
166
170
|
The icon and the sheet take the look of the page they sit on: its fonts, ground, ink, the fill
|
|
167
171
|
and shape of its main button, and its corner radius. To pin a role, set a custom property on
|
|
@@ -174,6 +178,12 @@ and shape of its main button, and its corner radius. To pin a role, set a custom
|
|
|
174
178
|
}
|
|
175
179
|
```
|
|
176
180
|
|
|
181
|
+
The sheet's type comes from the words the flow is drawing, not from `<body>`: the title takes
|
|
182
|
+
the face, weight and letter spacing of the flow's largest heading, the note and the field take
|
|
183
|
+
the face most of the flow's smaller text is set in, and the send button takes the face, weight,
|
|
184
|
+
letter spacing and case of the flow's main button. Pin `--fs-feedback-font` or
|
|
185
|
+
`--fs-feedback-font-display` when the reading is off.
|
|
186
|
+
|
|
177
187
|
The button floats over the flow and belongs to the person using it. They can drag it anywhere,
|
|
178
188
|
and it settles on the nearer side. A flick at an edge, or a drag into one, docks it there as a
|
|
179
189
|
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.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;
|
|
@@ -159,6 +161,46 @@ var CanvasWidgetSDK = (() => {
|
|
|
159
161
|
});
|
|
160
162
|
return best;
|
|
161
163
|
};
|
|
164
|
+
var TEXT_SAMPLE_LIMIT = 600;
|
|
165
|
+
var DISPLAY_MIN_PX = 22;
|
|
166
|
+
var sizeOf = (style) => {
|
|
167
|
+
const n = parseFloat(style.fontSize);
|
|
168
|
+
return Number.isFinite(n) && n > 0 ? n : 16;
|
|
169
|
+
};
|
|
170
|
+
var textSamples = (doc) => {
|
|
171
|
+
const out = [];
|
|
172
|
+
const all = doc.body.querySelectorAll("*");
|
|
173
|
+
for (let i = 0; i < all.length && out.length < TEXT_SAMPLE_LIMIT; i += 1) {
|
|
174
|
+
const el = all[i];
|
|
175
|
+
if (el.tagName === "SCRIPT" || el.tagName === "STYLE" || inOurs(el)) continue;
|
|
176
|
+
let chars = 0;
|
|
177
|
+
for (const node of Array.from(el.childNodes)) {
|
|
178
|
+
if (node.nodeType === 3) chars += (node.textContent ?? "").trim().length;
|
|
179
|
+
}
|
|
180
|
+
if (chars === 0 || !visible(el)) continue;
|
|
181
|
+
const style = styleOf(el);
|
|
182
|
+
if (!style.fontFamily) continue;
|
|
183
|
+
out.push({
|
|
184
|
+
family: style.fontFamily,
|
|
185
|
+
size: sizeOf(style),
|
|
186
|
+
chars,
|
|
187
|
+
style,
|
|
188
|
+
heading: /^H[1-3]$/.test(el.tagName)
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
};
|
|
193
|
+
var trackingEm = (style, min, max, fallback) => {
|
|
194
|
+
const px = style ? parseFloat(style.letterSpacing) : NaN;
|
|
195
|
+
const em = style && Number.isFinite(px) ? px / sizeOf(style) : fallback;
|
|
196
|
+
const held = Math.min(max, Math.max(min, em));
|
|
197
|
+
return held === 0 ? "0em" : `${Math.round(held * 1e3) / 1e3}em`;
|
|
198
|
+
};
|
|
199
|
+
var weightIn = (style, min, max, fallback) => {
|
|
200
|
+
const raw = style?.fontWeight ?? "";
|
|
201
|
+
const n = raw === "bold" ? 700 : raw === "normal" ? 400 : parseFloat(raw);
|
|
202
|
+
return Number.isFinite(n) ? String(Math.min(max, Math.max(min, n))) : fallback;
|
|
203
|
+
};
|
|
162
204
|
function readFlowTheme(doc = document) {
|
|
163
205
|
const body = doc.body;
|
|
164
206
|
const stage = body.querySelector(":scope > :not(script):not(style):not([data-fs-feedback-host])");
|
|
@@ -181,12 +223,32 @@ var CanvasWidgetSDK = (() => {
|
|
|
181
223
|
const pinnedCtaInk = parseColor(rootVar(doc, ["--fs-feedback-cta-ink"]));
|
|
182
224
|
const sampledCtaInk = !pinnedCta && sampledCta && buttonStyle ? parseColor(buttonStyle.color) : null;
|
|
183
225
|
const ctaInk = pinnedCtaInk ?? (sampledCtaInk && contrast(sampledCtaInk, cta) >= 3 ? sampledCtaInk : null) ?? (luminance(cta) > 0.4 ? BLACK : WHITE);
|
|
184
|
-
const
|
|
226
|
+
const samples = textSamples(doc);
|
|
227
|
+
const headings = Array.from(doc.querySelectorAll('h1, h2, [class*="title"]')).filter(
|
|
185
228
|
(el) => !inOurs(el) && visible(el)
|
|
186
229
|
);
|
|
187
|
-
const
|
|
188
|
-
|
|
230
|
+
const largest = (items, size) => items.reduce(
|
|
231
|
+
(best, item) => best === null || size(item) > size(best) ? item : best,
|
|
232
|
+
null
|
|
233
|
+
);
|
|
234
|
+
const bigHeading = largest(
|
|
235
|
+
headings.filter((el) => sizeOf(styleOf(el)) >= DISPLAY_MIN_PX),
|
|
236
|
+
(el) => sizeOf(styleOf(el))
|
|
237
|
+
);
|
|
238
|
+
const bigLine = largest(
|
|
239
|
+
samples.filter((t) => t.size >= DISPLAY_MIN_PX && t.chars >= 3),
|
|
240
|
+
(t) => t.size
|
|
241
|
+
);
|
|
242
|
+
const headingStyle = bigHeading ? styleOf(bigHeading) : bigLine?.style ?? (headings[0] ? styleOf(headings[0]) : null);
|
|
243
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
244
|
+
for (const t of samples) {
|
|
245
|
+
if (t.size < DISPLAY_MIN_PX && !t.heading)
|
|
246
|
+
byFamily.set(t.family, (byFamily.get(t.family) ?? 0) + t.chars);
|
|
247
|
+
}
|
|
248
|
+
const readBody = largest(Array.from(byFamily), ([, chars]) => chars)?.[0] ?? "";
|
|
249
|
+
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
250
|
const fontDisplay = rootVar(doc, ["--fs-feedback-font-display"]) || headingStyle?.fontFamily || rootVar(doc, ["--sb-font-display", "--sb-font-title", "--font-display"]) || fontBody;
|
|
251
|
+
const buttonType = buttonStyle?.fontFamily ? buttonStyle : null;
|
|
190
252
|
const field = Array.from(
|
|
191
253
|
doc.querySelectorAll('textarea, input[type="text"], input:not([type])')
|
|
192
254
|
).find((el) => !inOurs(el) && visible(el));
|
|
@@ -210,6 +272,11 @@ var CanvasWidgetSDK = (() => {
|
|
|
210
272
|
fontBody,
|
|
211
273
|
fontDisplay,
|
|
212
274
|
displayWeight: headingStyle?.fontWeight || "500",
|
|
275
|
+
displayTracking: trackingEm(headingStyle, -0.04, 0.02, -0.01),
|
|
276
|
+
fontButton: buttonType?.fontFamily || fontBody,
|
|
277
|
+
buttonWeight: weightIn(buttonType, 500, 800, "600"),
|
|
278
|
+
buttonTracking: trackingEm(buttonType, -0.02, 0.08, 0),
|
|
279
|
+
buttonTransform: buttonType?.textTransform === "uppercase" ? "uppercase" : "none",
|
|
213
280
|
radius,
|
|
214
281
|
buttonRadius: buttonRadius === null ? "999px" : `${Math.min(buttonRadius, 999)}px`,
|
|
215
282
|
dark
|
|
@@ -235,9 +302,10 @@ var CanvasWidgetSDK = (() => {
|
|
|
235
302
|
return null;
|
|
236
303
|
}
|
|
237
304
|
async function captureViewport(ground) {
|
|
238
|
-
const { domToCanvas: domToCanvas2 } = await Promise.resolve().then(() => (init_modern_screenshot(), modern_screenshot_exports));
|
|
239
305
|
const width = window.innerWidth;
|
|
240
306
|
const height = window.innerHeight;
|
|
307
|
+
const { scrollX, scrollY } = window;
|
|
308
|
+
const { domToCanvas: domToCanvas2 } = await Promise.resolve().then(() => (init_modern_screenshot(), modern_screenshot_exports));
|
|
241
309
|
const scale = Math.min(
|
|
242
310
|
window.devicePixelRatio || 1,
|
|
243
311
|
SCREENSHOT_MAX_SIDE / Math.max(width, height)
|
|
@@ -247,7 +315,7 @@ var CanvasWidgetSDK = (() => {
|
|
|
247
315
|
height,
|
|
248
316
|
scale,
|
|
249
317
|
backgroundColor: ground,
|
|
250
|
-
style: { transform: `translate(${-
|
|
318
|
+
style: { transform: `translate(${-scrollX}px, ${-scrollY}px)` },
|
|
251
319
|
filter: (node) => !(node instanceof Element && node.hasAttribute(FEEDBACK_HOST_ATTR))
|
|
252
320
|
});
|
|
253
321
|
return encodeScreenshot(canvas);
|
|
@@ -441,9 +509,45 @@ var CanvasWidgetSDK = (() => {
|
|
|
441
509
|
font-family: var(--font-body); font-size: 15px; line-height: 1.4;
|
|
442
510
|
}
|
|
443
511
|
.grab { width: 36px; height: 4px; border-radius: 2px; background: var(--line); align-self: center; }
|
|
444
|
-
h2 {
|
|
445
|
-
|
|
446
|
-
|
|
512
|
+
h2 {
|
|
513
|
+
margin: 2px 0 0; font-family: var(--font-display); font-weight: var(--display-weight);
|
|
514
|
+
font-size: 22px; line-height: 1.15; letter-spacing: var(--display-tracking); text-wrap: balance;
|
|
515
|
+
}
|
|
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; }
|
|
447
551
|
.link { background: none; border: 0; padding: 0; font: inherit; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
|
|
448
552
|
textarea {
|
|
449
553
|
width: 100%; min-height: 84px; resize: none; font: inherit; font-size: 16px; color: var(--ink);
|
|
@@ -451,10 +555,24 @@ textarea {
|
|
|
451
555
|
}
|
|
452
556
|
textarea::placeholder { color: var(--ink-dim); }
|
|
453
557
|
.send {
|
|
454
|
-
font:
|
|
558
|
+
font-family: var(--font-button); font-weight: var(--button-weight); font-size: 17px; line-height: 1.2;
|
|
559
|
+
letter-spacing: var(--button-tracking); text-transform: var(--button-transform);
|
|
560
|
+
cursor: pointer; border: 0; padding: 15px 14px;
|
|
455
561
|
background: var(--cta); color: var(--cta-ink); border-radius: var(--button-radius);
|
|
456
562
|
}
|
|
563
|
+
.send { display: flex; align-items: center; justify-content: center; gap: 9px; }
|
|
457
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; }
|
|
458
576
|
.status { margin: 0; font-size: 13px; color: var(--ink-dim); min-height: 1.4em; }
|
|
459
577
|
[hidden] { display: none !important; }
|
|
460
578
|
@media (prefers-reduced-motion: no-preference) {
|
|
@@ -464,11 +582,31 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
464
582
|
}
|
|
465
583
|
.bug svg { transition: transform 0.1s ease-out, width 0.22s ease, height 0.22s ease; }
|
|
466
584
|
.sheet { animation: rise 0.22s ease-out; }
|
|
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; } }
|
|
467
598
|
@keyframes rise { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
468
599
|
}
|
|
469
600
|
`;
|
|
470
601
|
var mounted = null;
|
|
471
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
|
+
};
|
|
472
610
|
var SCROLL_OPTIONS = { capture: true, passive: true };
|
|
473
611
|
function mountFlowFeedback(options) {
|
|
474
612
|
if (typeof document === "undefined" || !document.body) return null;
|
|
@@ -481,14 +619,22 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
481
619
|
<button type="button" class="bug" data-state="float" aria-label="Send feedback on this page">${BUG_ICON}</button>
|
|
482
620
|
<div class="probe" aria-hidden="true"></div>
|
|
483
621
|
<div class="scrim" hidden></div>
|
|
484
|
-
<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>
|
|
485
624
|
<div class="grab" aria-hidden="true"></div>
|
|
486
|
-
<
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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>
|
|
492
638
|
</div>`;
|
|
493
639
|
const q = (selector) => root.querySelector(selector);
|
|
494
640
|
const bug = q(".bug");
|
|
@@ -496,7 +642,15 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
496
642
|
const sheet = q(".sheet");
|
|
497
643
|
const shot = q(".shot");
|
|
498
644
|
const img = q(".shot img");
|
|
645
|
+
const shotLabel = q(".shot-label");
|
|
499
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");
|
|
500
654
|
const text = q("textarea");
|
|
501
655
|
const status = q(".status");
|
|
502
656
|
const send = q(".send");
|
|
@@ -706,6 +860,11 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
706
860
|
"--font-body": theme.fontBody,
|
|
707
861
|
"--font-display": theme.fontDisplay,
|
|
708
862
|
"--display-weight": theme.displayWeight,
|
|
863
|
+
"--display-tracking": theme.displayTracking,
|
|
864
|
+
"--font-button": theme.fontButton,
|
|
865
|
+
"--button-weight": theme.buttonWeight,
|
|
866
|
+
"--button-tracking": theme.buttonTracking,
|
|
867
|
+
"--button-transform": theme.buttonTransform,
|
|
709
868
|
"--radius": theme.radius,
|
|
710
869
|
"--button-radius": theme.buttonRadius
|
|
711
870
|
};
|
|
@@ -715,46 +874,104 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
715
874
|
const sync = () => {
|
|
716
875
|
send.disabled = busy || text.value.trim().length === 0;
|
|
717
876
|
};
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
877
|
+
let round = 0;
|
|
878
|
+
let pending = null;
|
|
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;
|
|
722
887
|
else img.removeAttribute("src");
|
|
723
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
|
+
};
|
|
724
927
|
const close = () => {
|
|
725
928
|
if (closeTimer) clearTimeout(closeTimer);
|
|
726
929
|
closeTimer = null;
|
|
930
|
+
round += 1;
|
|
931
|
+
pending = null;
|
|
727
932
|
sheet.hidden = true;
|
|
728
933
|
scrim.hidden = true;
|
|
729
934
|
bug.hidden = false;
|
|
730
|
-
|
|
935
|
+
setBusy(false);
|
|
936
|
+
setShot("none");
|
|
937
|
+
setView("form");
|
|
731
938
|
text.value = "";
|
|
732
939
|
status.textContent = "";
|
|
733
|
-
busy = false;
|
|
734
940
|
sync();
|
|
735
941
|
schedulePlace();
|
|
736
942
|
};
|
|
737
|
-
const open =
|
|
738
|
-
if (!sheet.hidden
|
|
739
|
-
busy
|
|
943
|
+
const open = () => {
|
|
944
|
+
if (!sheet.hidden) return pending ?? Promise.resolve();
|
|
945
|
+
if (busy) return Promise.resolve();
|
|
740
946
|
paint();
|
|
741
947
|
bug.hidden = true;
|
|
742
|
-
|
|
743
|
-
const url = await withTimeout(Promise.resolve().then(capture), CAPTURE_TIMEOUT_MS);
|
|
744
|
-
busy = false;
|
|
745
|
-
setShot(url);
|
|
948
|
+
setView("form");
|
|
746
949
|
tuck.hidden = spot.state !== "float";
|
|
747
950
|
scrim.hidden = false;
|
|
748
951
|
sheet.hidden = false;
|
|
749
952
|
sync();
|
|
750
|
-
|
|
953
|
+
return take();
|
|
751
954
|
};
|
|
752
955
|
const submit = async () => {
|
|
753
956
|
const message = text.value.trim();
|
|
754
957
|
if (!message || busy) return;
|
|
755
|
-
|
|
756
|
-
sync();
|
|
958
|
+
setBusy(true);
|
|
757
959
|
status.textContent = "";
|
|
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
|
+
}
|
|
758
975
|
try {
|
|
759
976
|
await options.submit({
|
|
760
977
|
message,
|
|
@@ -763,21 +980,27 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
763
980
|
pagePath: window.location.pathname.slice(-200),
|
|
764
981
|
viewport: { width: window.innerWidth, height: window.innerHeight }
|
|
765
982
|
});
|
|
766
|
-
|
|
983
|
+
if (mine !== round) return;
|
|
984
|
+
setView("sent");
|
|
767
985
|
closeTimer = setTimeout(() => {
|
|
768
986
|
close();
|
|
769
987
|
bug.focus();
|
|
770
|
-
},
|
|
988
|
+
}, SENT_HOLD_MS);
|
|
771
989
|
} catch {
|
|
772
|
-
|
|
990
|
+
if (mine !== round) return;
|
|
991
|
+
setBusy(false);
|
|
773
992
|
status.textContent = "That did not send. Try again.";
|
|
774
|
-
sync();
|
|
775
993
|
}
|
|
776
994
|
};
|
|
777
995
|
bug.addEventListener("click", () => {
|
|
778
996
|
if (!suppressClick) void open();
|
|
779
997
|
});
|
|
780
998
|
bug.addEventListener("pointerdown", onDown);
|
|
999
|
+
if (!options.capture)
|
|
1000
|
+
bug.addEventListener("pointerdown", () => void Promise.resolve().then(() => (init_modern_screenshot(), modern_screenshot_exports)).catch(() => {
|
|
1001
|
+
}), {
|
|
1002
|
+
once: true
|
|
1003
|
+
});
|
|
781
1004
|
bug.addEventListener("pointermove", onMove);
|
|
782
1005
|
bug.addEventListener("pointerup", onUp);
|
|
783
1006
|
bug.addEventListener("pointercancel", onUp);
|
|
@@ -797,7 +1020,19 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
797
1020
|
);
|
|
798
1021
|
});
|
|
799
1022
|
scrim.addEventListener("click", close);
|
|
800
|
-
remove.addEventListener("click", () =>
|
|
1023
|
+
remove.addEventListener("click", () => {
|
|
1024
|
+
round += 1;
|
|
1025
|
+
pending = null;
|
|
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
|
+
}
|
|
1035
|
+
});
|
|
801
1036
|
text.addEventListener("input", sync);
|
|
802
1037
|
send.addEventListener("click", () => void submit());
|
|
803
1038
|
sheet.addEventListener("keydown", (event) => {
|