@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.
@@ -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 takes a screenshot of the page as the person
6
- * sees it, then opens a sheet: the thumbnail, one field, one button. The note goes out through
7
- * the flow's own session (`session.request('feedback', …)`).
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;
@@ -1 +1 @@
1
- {"version":3,"file":"feedback.d.ts","sourceRoot":"","sources":["../src/feedback.ts"],"names":[],"mappings":"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"}
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.2
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 = 4e3;
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 heading = Array.from(doc.querySelectorAll('h1, h2, [class*="title"]')).find(
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 headingStyle = heading ? styleOf(heading) : null;
150
- const fontBody = rootVar(doc, ["--fs-feedback-font"]) || (stage ? styleOf(stage).fontFamily : "") || bodyStyle.fontFamily || rootVar(doc, ["--sb-font-body", "--font-body"]) || "system-ui, sans-serif";
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(${-window.scrollX}px, ${-window.scrollY}px)` },
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 { margin: 0; font-family: var(--font-display); font-weight: var(--display-weight); font-size: 19px; line-height: 1.2; }
407
- .shot { display: flex; gap: 12px; align-items: center; font-size: 13px; color: var(--ink-dim); }
408
- .shot img { width: 50px; max-height: 108px; object-fit: cover; object-position: top; border-radius: 8px; border: 1px solid var(--line); }
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: inherit; font-weight: 500; cursor: pointer; border: 0; padding: 14px;
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="sheet" role="dialog" aria-modal="true" aria-labelledby="fsf-title" hidden>
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
- <h2 id="fsf-title">Feedback on this page</h2>
449
- <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>
450
- <textarea aria-labelledby="fsf-title" placeholder="What should change?" maxlength="2000"></textarea>
451
- <p class="status" role="status" aria-live="polite"></p>
452
- <button type="button" class="send" disabled>Send feedback</button>
453
- <button type="button" class="link tuck">Tuck the button to the side</button>
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
- const setShot = (url) => {
681
- screenshot = url;
682
- shot.hidden = url === null;
683
- if (url) img.src = url;
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
- setShot(null);
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 = async () => {
700
- if (!sheet.hidden || busy) return;
701
- busy = true;
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
- const capture = options.capture ?? (() => captureViewport(theme.ground));
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
- text.focus();
915
+ return take();
713
916
  };
714
917
  const submit = async () => {
715
918
  const message = text.value.trim();
716
919
  if (!message || busy) return;
717
- busy = true;
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
- status.textContent = "Sent.";
945
+ if (mine !== round) return;
946
+ setView("sent");
729
947
  closeTimer = setTimeout(() => {
730
948
  close();
731
949
  bug.focus();
732
- }, 1200);
950
+ }, SENT_HOLD_MS);
733
951
  } catch {
734
- busy = false;
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", () => setShot(null));
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) => {