@goflowstate/widget-sdk 0.3.0 → 0.3.2
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 +36 -0
- package/dist/canvas-widget-sdk.js +819 -2
- package/dist/canvas-widget-sdk.js.map +4 -4
- package/dist/feedback.d.ts +152 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.test.d.ts +2 -0
- package/dist/feedback.test.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +809 -3
- package/dist/index.mjs.map +4 -4
- package/dist/session.d.ts +2 -0
- package/dist/session.d.ts.map +1 -1
- package/package.json +5 -2
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @goflowstate/widget-sdk v0.3.
|
|
1
|
+
// @goflowstate/widget-sdk v0.3.2
|
|
2
2
|
// https://github.com/goflowstate/goflowstate-sdk
|
|
3
3
|
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -39,6 +39,787 @@ function createServiceClient(opts) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// src/feedback.ts
|
|
43
|
+
var FEEDBACK_HOST_ATTR = "data-fs-feedback-host";
|
|
44
|
+
var SCREENSHOT_MAX_BYTES = 15e4;
|
|
45
|
+
var SCREENSHOT_MAX_SIDE = 1280;
|
|
46
|
+
var CAPTURE_TIMEOUT_MS = 4e3;
|
|
47
|
+
function parseColor(raw) {
|
|
48
|
+
const value = (raw ?? "").trim().toLowerCase();
|
|
49
|
+
if (!value || value === "transparent") return null;
|
|
50
|
+
const hex = /^#([0-9a-f]{3,8})$/.exec(value);
|
|
51
|
+
if (hex) {
|
|
52
|
+
const h = hex[1];
|
|
53
|
+
const full = h.length <= 4 ? h.split("").map((c) => c + c).join("") : h;
|
|
54
|
+
if (full.length !== 6 && full.length !== 8) return null;
|
|
55
|
+
const n = (i) => parseInt(full.slice(i, i + 2), 16);
|
|
56
|
+
return [n(0), n(2), n(4), full.length === 8 ? n(6) / 255 : 1];
|
|
57
|
+
}
|
|
58
|
+
const fn = /^rgba?\(([^)]+)\)$/.exec(value);
|
|
59
|
+
if (!fn) return null;
|
|
60
|
+
const parts = fn[1].split(/[\s,/]+/).filter(Boolean);
|
|
61
|
+
if (parts.length < 3) return null;
|
|
62
|
+
const channel = (p) => p.endsWith("%") ? parseFloat(p) * 255 / 100 : parseFloat(p);
|
|
63
|
+
const alpha = parts[3] === void 0 ? 1 : parts[3].endsWith("%") ? parseFloat(parts[3]) / 100 : parseFloat(parts[3]);
|
|
64
|
+
const out = [channel(parts[0]), channel(parts[1]), channel(parts[2]), alpha];
|
|
65
|
+
return out.some((v) => Number.isNaN(v)) ? null : out;
|
|
66
|
+
}
|
|
67
|
+
var opaque = (c) => c !== null && c[3] >= 0.9;
|
|
68
|
+
var css = (c, alpha = c[3]) => alpha >= 1 ? `rgb(${Math.round(c[0])}, ${Math.round(c[1])}, ${Math.round(c[2])})` : `rgba(${Math.round(c[0])}, ${Math.round(c[1])}, ${Math.round(c[2])}, ${alpha})`;
|
|
69
|
+
var mix = (a, b, t) => [
|
|
70
|
+
a[0] + (b[0] - a[0]) * t,
|
|
71
|
+
a[1] + (b[1] - a[1]) * t,
|
|
72
|
+
a[2] + (b[2] - a[2]) * t,
|
|
73
|
+
1
|
|
74
|
+
];
|
|
75
|
+
var luminance = (c) => {
|
|
76
|
+
const lin = (v) => {
|
|
77
|
+
const s = v / 255;
|
|
78
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
79
|
+
};
|
|
80
|
+
return 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);
|
|
81
|
+
};
|
|
82
|
+
var contrast = (a, b) => {
|
|
83
|
+
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
84
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
85
|
+
};
|
|
86
|
+
var WHITE = [255, 255, 255, 1];
|
|
87
|
+
var BLACK = [17, 17, 20, 1];
|
|
88
|
+
var styleOf = (el) => getComputedStyle(el);
|
|
89
|
+
var rootVar = (doc, names) => {
|
|
90
|
+
for (const el of [doc.documentElement, doc.body]) {
|
|
91
|
+
if (!el) continue;
|
|
92
|
+
const style = styleOf(el);
|
|
93
|
+
for (const name of names) {
|
|
94
|
+
const value = style.getPropertyValue(name).trim();
|
|
95
|
+
if (value) return value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return "";
|
|
99
|
+
};
|
|
100
|
+
var visible = (el) => {
|
|
101
|
+
const rect = el.getBoundingClientRect();
|
|
102
|
+
const style = styleOf(el);
|
|
103
|
+
return style.display !== "none" && style.visibility !== "hidden" && rect.width * rect.height >= 0;
|
|
104
|
+
};
|
|
105
|
+
var inOurs = (el) => el.closest(`[${FEEDBACK_HOST_ATTR}]`) !== null;
|
|
106
|
+
var primaryButton = (doc, ground) => {
|
|
107
|
+
let best = null;
|
|
108
|
+
let bestArea = -1;
|
|
109
|
+
doc.querySelectorAll(
|
|
110
|
+
'button, [role="button"], input[type="submit"], a[class*="btn"], a[class*="cta"]'
|
|
111
|
+
).forEach((el) => {
|
|
112
|
+
if (inOurs(el) || !visible(el)) return;
|
|
113
|
+
const fill = parseColor(styleOf(el).backgroundColor);
|
|
114
|
+
if (!opaque(fill) || contrast(fill, ground) < 1.25) return;
|
|
115
|
+
const rect = el.getBoundingClientRect();
|
|
116
|
+
const area = rect.width * rect.height;
|
|
117
|
+
if (area > bestArea) {
|
|
118
|
+
best = el;
|
|
119
|
+
bestArea = area;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return best;
|
|
123
|
+
};
|
|
124
|
+
function readFlowTheme(doc = document) {
|
|
125
|
+
const body = doc.body;
|
|
126
|
+
const stage = body.querySelector(":scope > :not(script):not(style):not([data-fs-feedback-host])");
|
|
127
|
+
const bodyStyle = styleOf(body);
|
|
128
|
+
const sampledGround = [stage, body, doc.documentElement].map((el) => el ? parseColor(styleOf(el).backgroundColor) : null).find(opaque);
|
|
129
|
+
const sampledInk = parseColor((stage ? styleOf(stage) : bodyStyle).color) ?? parseColor(bodyStyle.color);
|
|
130
|
+
const tokenGround = parseColor(rootVar(doc, ["--sb-ground", "--sb-bg", "--color-ink-050"]));
|
|
131
|
+
const ink0 = parseColor(rootVar(doc, ["--fs-feedback-ink"])) ?? sampledInk;
|
|
132
|
+
let ground = parseColor(rootVar(doc, ["--fs-feedback-ground"])) ?? sampledGround ?? (tokenGround && opaque(tokenGround) ? tokenGround : null) ?? (ink0 && luminance(ink0) > 0.5 ? BLACK : WHITE);
|
|
133
|
+
ground = [ground[0], ground[1], ground[2], 1];
|
|
134
|
+
const dark = luminance(ground) < 0.35;
|
|
135
|
+
let ink = ink0 && contrast([ink0[0], ink0[1], ink0[2], 1], ground) >= 4.5 ? ink0 : dark ? WHITE : BLACK;
|
|
136
|
+
ink = [ink[0], ink[1], ink[2], 1];
|
|
137
|
+
const button = primaryButton(doc, ground);
|
|
138
|
+
const buttonStyle = button ? styleOf(button) : null;
|
|
139
|
+
const pinnedCta = parseColor(rootVar(doc, ["--fs-feedback-cta"]));
|
|
140
|
+
const tokenCta = parseColor(rootVar(doc, ["--sb-cta", "--sb-accent", "--color-cream"]));
|
|
141
|
+
const sampledCta = buttonStyle ? parseColor(buttonStyle.backgroundColor) : null;
|
|
142
|
+
const cta = pinnedCta ?? sampledCta ?? (tokenCta && contrast(tokenCta, ground) >= 1.25 ? tokenCta : null) ?? ink;
|
|
143
|
+
const pinnedCtaInk = parseColor(rootVar(doc, ["--fs-feedback-cta-ink"]));
|
|
144
|
+
const sampledCtaInk = !pinnedCta && sampledCta && buttonStyle ? parseColor(buttonStyle.color) : null;
|
|
145
|
+
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(
|
|
147
|
+
(el) => !inOurs(el) && visible(el)
|
|
148
|
+
);
|
|
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";
|
|
151
|
+
const fontDisplay = rootVar(doc, ["--fs-feedback-font-display"]) || headingStyle?.fontFamily || rootVar(doc, ["--sb-font-display", "--sb-font-title", "--font-display"]) || fontBody;
|
|
152
|
+
const field = Array.from(
|
|
153
|
+
doc.querySelectorAll('textarea, input[type="text"], input:not([type])')
|
|
154
|
+
).find((el) => !inOurs(el) && visible(el));
|
|
155
|
+
const px = (value) => {
|
|
156
|
+
const n = parseFloat(value ?? "");
|
|
157
|
+
return Number.isFinite(n) ? n : null;
|
|
158
|
+
};
|
|
159
|
+
const cornerOf = (style) => style ? px(style.borderTopLeftRadius || style.borderRadius) : null;
|
|
160
|
+
const buttonRadius = cornerOf(buttonStyle);
|
|
161
|
+
const fieldRadius = cornerOf(field ? styleOf(field) : null);
|
|
162
|
+
const radius = rootVar(doc, ["--fs-feedback-radius"]) || `${Math.min(fieldRadius ?? Math.min(buttonRadius ?? 16, 22), 28)}px`;
|
|
163
|
+
return {
|
|
164
|
+
ground: css(ground),
|
|
165
|
+
surface: css(dark ? mix(ground, WHITE, 0.07) : mix(ground, WHITE, 0.6)),
|
|
166
|
+
ink: css(ink),
|
|
167
|
+
inkDim: css(mix(ink, ground, 0.32)),
|
|
168
|
+
line: css(ink, 0.16),
|
|
169
|
+
fill: css(ink, 0.05),
|
|
170
|
+
cta: css([cta[0], cta[1], cta[2], 1]),
|
|
171
|
+
ctaInk: css([ctaInk[0], ctaInk[1], ctaInk[2], 1]),
|
|
172
|
+
fontBody,
|
|
173
|
+
fontDisplay,
|
|
174
|
+
displayWeight: headingStyle?.fontWeight || "500",
|
|
175
|
+
radius,
|
|
176
|
+
buttonRadius: buttonRadius === null ? "999px" : `${Math.min(buttonRadius, 999)}px`,
|
|
177
|
+
dark
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
var dataUrlBytes = (url) => Math.floor((url.length - url.indexOf(",") - 1) * 3 / 4);
|
|
181
|
+
function encodeScreenshot(canvas) {
|
|
182
|
+
let source = canvas;
|
|
183
|
+
for (let pass = 0; pass < 3; pass += 1) {
|
|
184
|
+
for (const quality of [0.82, 0.7, 0.58, 0.46, 0.34]) {
|
|
185
|
+
const url = source.toDataURL("image/jpeg", quality);
|
|
186
|
+
if (url.startsWith("data:image/jpeg") && dataUrlBytes(url) <= SCREENSHOT_MAX_BYTES)
|
|
187
|
+
return url;
|
|
188
|
+
}
|
|
189
|
+
const smaller = document.createElement("canvas");
|
|
190
|
+
smaller.width = Math.max(1, Math.round(source.width * 0.75));
|
|
191
|
+
smaller.height = Math.max(1, Math.round(source.height * 0.75));
|
|
192
|
+
const ctx = smaller.getContext("2d");
|
|
193
|
+
if (!ctx) return null;
|
|
194
|
+
ctx.drawImage(source, 0, 0, smaller.width, smaller.height);
|
|
195
|
+
source = smaller;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
async function captureViewport(ground) {
|
|
200
|
+
const { domToCanvas } = await import("modern-screenshot");
|
|
201
|
+
const width = window.innerWidth;
|
|
202
|
+
const height = window.innerHeight;
|
|
203
|
+
const scale = Math.min(
|
|
204
|
+
window.devicePixelRatio || 1,
|
|
205
|
+
SCREENSHOT_MAX_SIDE / Math.max(width, height)
|
|
206
|
+
);
|
|
207
|
+
const canvas = await domToCanvas(document.documentElement, {
|
|
208
|
+
width,
|
|
209
|
+
height,
|
|
210
|
+
scale,
|
|
211
|
+
backgroundColor: ground,
|
|
212
|
+
style: { transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)` },
|
|
213
|
+
filter: (node) => !(node instanceof Element && node.hasAttribute(FEEDBACK_HOST_ATTR))
|
|
214
|
+
});
|
|
215
|
+
return encodeScreenshot(canvas);
|
|
216
|
+
}
|
|
217
|
+
var withTimeout = (work, ms) => new Promise((resolve) => {
|
|
218
|
+
const timer = setTimeout(() => resolve(null), ms);
|
|
219
|
+
work.then(
|
|
220
|
+
(value) => {
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
resolve(value);
|
|
223
|
+
},
|
|
224
|
+
() => {
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
resolve(null);
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
var PLACEMENT_BASE = 16;
|
|
231
|
+
var PLACEMENT_STEP = 8;
|
|
232
|
+
var PLACEMENT_PAD = 8;
|
|
233
|
+
var PLACEMENT_MAX_SHARE = 0.6;
|
|
234
|
+
var PLACEMENT_READ_COST = 240;
|
|
235
|
+
var EMPTY = 0;
|
|
236
|
+
var READ = 1;
|
|
237
|
+
var TAP = 2;
|
|
238
|
+
var SAMPLES = 5;
|
|
239
|
+
function choosePlacement(box, viewportHeight, at) {
|
|
240
|
+
const max = Math.max(PLACEMENT_BASE, viewportHeight * PLACEMENT_MAX_SHARE);
|
|
241
|
+
const span = box.size + PLACEMENT_PAD * 2;
|
|
242
|
+
const offsets = Array.from({ length: SAMPLES }, (_, i) => span * i / (SAMPLES - 1));
|
|
243
|
+
const x0 = box.left - PLACEMENT_PAD;
|
|
244
|
+
let best = PLACEMENT_BASE;
|
|
245
|
+
let bestCost = Infinity;
|
|
246
|
+
for (let bottom = PLACEMENT_BASE; bottom <= max; bottom += PLACEMENT_STEP) {
|
|
247
|
+
if (bottom >= bestCost) break;
|
|
248
|
+
const y0 = box.top - (bottom - box.bottom) - PLACEMENT_PAD;
|
|
249
|
+
let worst = EMPTY;
|
|
250
|
+
for (const dy of offsets) {
|
|
251
|
+
for (const dx of offsets) {
|
|
252
|
+
const found = at(x0 + dx, y0 + dy);
|
|
253
|
+
if (found > worst) worst = found;
|
|
254
|
+
if (worst === TAP) break;
|
|
255
|
+
}
|
|
256
|
+
if (worst === TAP) break;
|
|
257
|
+
}
|
|
258
|
+
if (worst === TAP) continue;
|
|
259
|
+
const cost = bottom + (worst === READ ? PLACEMENT_READ_COST : 0);
|
|
260
|
+
if (cost < bestCost) {
|
|
261
|
+
bestCost = cost;
|
|
262
|
+
best = bottom;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return best;
|
|
266
|
+
}
|
|
267
|
+
var TAPPABLE = 'a[href],button,input,select,textarea,summary,label,[role="button"],[role="link"],[role="tab"],[role="switch"],[role="checkbox"],[role="radio"],[role="slider"],[role="option"],[tabindex]:not([tabindex="-1"]),[contenteditable=""],[contenteditable="true"],[onclick]';
|
|
268
|
+
var PICTURE = "img,svg,canvas,video,picture";
|
|
269
|
+
var SURFACE_SHARE = 0.8;
|
|
270
|
+
function occupancyReader(host, doc = document) {
|
|
271
|
+
if (typeof doc.elementsFromPoint !== "function") return () => EMPTY;
|
|
272
|
+
const view = doc.defaultView;
|
|
273
|
+
const width = view?.innerWidth ?? 1;
|
|
274
|
+
const height = view?.innerHeight ?? 1;
|
|
275
|
+
const seen = /* @__PURE__ */ new Map();
|
|
276
|
+
const read = (el) => {
|
|
277
|
+
const known = seen.get(el);
|
|
278
|
+
if (known) return known;
|
|
279
|
+
const style = view ? view.getComputedStyle(el) : null;
|
|
280
|
+
let tap = el.matches(TAPPABLE) || style?.cursor === "pointer";
|
|
281
|
+
if (tap) {
|
|
282
|
+
const r = el.getBoundingClientRect();
|
|
283
|
+
if (r.width * r.height >= width * height * SURFACE_SHARE) tap = false;
|
|
284
|
+
}
|
|
285
|
+
const text = [];
|
|
286
|
+
if (typeof doc.createRange === "function") {
|
|
287
|
+
for (const node of Array.from(el.childNodes)) {
|
|
288
|
+
if (node.nodeType !== 3 || !node.nodeValue || !node.nodeValue.trim()) continue;
|
|
289
|
+
const range = doc.createRange();
|
|
290
|
+
range.selectNodeContents(node);
|
|
291
|
+
if (typeof range.getClientRects === "function")
|
|
292
|
+
text.push(...Array.from(range.getClientRects()));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const ground = parseColor(style?.backgroundColor);
|
|
296
|
+
const image = style?.backgroundImage ?? "";
|
|
297
|
+
const reading = {
|
|
298
|
+
tap,
|
|
299
|
+
picture: el.matches(PICTURE) || el.closest("svg") !== null,
|
|
300
|
+
text,
|
|
301
|
+
opaque: ground !== null && ground[3] >= 0.5 || image !== "" && image !== "none"
|
|
302
|
+
};
|
|
303
|
+
seen.set(el, reading);
|
|
304
|
+
return reading;
|
|
305
|
+
};
|
|
306
|
+
return (x, y) => {
|
|
307
|
+
const cx = Math.min(Math.max(x, 0), width - 1);
|
|
308
|
+
const cy = Math.min(Math.max(y, 0), height - 1);
|
|
309
|
+
const stack = doc.elementsFromPoint(cx, cy).filter((el) => el !== host && el !== doc.documentElement && el !== doc.body);
|
|
310
|
+
for (let el = stack[0] ?? null; el && el !== doc.body; el = el.parentElement) {
|
|
311
|
+
if (read(el).tap) return TAP;
|
|
312
|
+
}
|
|
313
|
+
for (const el of stack) {
|
|
314
|
+
const r = read(el);
|
|
315
|
+
if (r.picture || r.text.some((t) => cx >= t.left && cx <= t.right && cy >= t.top && cy <= t.bottom))
|
|
316
|
+
return READ;
|
|
317
|
+
if (r.opaque) break;
|
|
318
|
+
}
|
|
319
|
+
return EMPTY;
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
var BUTTON_SIZE = 44;
|
|
323
|
+
var TAB_WIDTH = 26;
|
|
324
|
+
var TAB_HEIGHT = 54;
|
|
325
|
+
var EDGE_MARGIN = 12;
|
|
326
|
+
var DOCK_PAST = 14;
|
|
327
|
+
var SPOT_KEY = "fs-feedback-spot";
|
|
328
|
+
var projectMomentum = (v, deceleration = 0.992) => v / 1e3 * deceleration / (1 - deceleration);
|
|
329
|
+
var rubberband = (overshoot, dimension, constant = 0.55) => overshoot * dimension * constant / (dimension + constant * Math.abs(overshoot));
|
|
330
|
+
var clampY = (y, height, view) => Math.min(
|
|
331
|
+
Math.max(y, view.top + EDGE_MARGIN),
|
|
332
|
+
Math.max(view.top + EDGE_MARGIN, view.height - view.bottom - EDGE_MARGIN - height)
|
|
333
|
+
);
|
|
334
|
+
function restingSpot(state, left, y, view) {
|
|
335
|
+
if (state === "float") {
|
|
336
|
+
return {
|
|
337
|
+
state,
|
|
338
|
+
x: left ? EDGE_MARGIN : view.width - BUTTON_SIZE - EDGE_MARGIN,
|
|
339
|
+
y: clampY(y, BUTTON_SIZE, view)
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return { state, x: left ? 0 : view.width - TAB_WIDTH, y: clampY(y, TAB_HEIGHT, view) };
|
|
343
|
+
}
|
|
344
|
+
function settleRelease(from, velocity, view) {
|
|
345
|
+
const py = from.y + projectMomentum(velocity.y);
|
|
346
|
+
if (from.state !== "float") return restingSpot(from.state, from.state === "dock-left", py, view);
|
|
347
|
+
const px = from.x + projectMomentum(velocity.x);
|
|
348
|
+
const maxX = view.width - BUTTON_SIZE;
|
|
349
|
+
const tabY = py + (BUTTON_SIZE - TAB_HEIGHT) / 2;
|
|
350
|
+
if (px < -DOCK_PAST || from.x < 3) return restingSpot("dock-left", true, tabY, view);
|
|
351
|
+
if (px > maxX + DOCK_PAST || from.x > maxX - 3)
|
|
352
|
+
return restingSpot("dock-right", false, tabY, view);
|
|
353
|
+
return restingSpot("float", px + BUTTON_SIZE / 2 < view.width / 2, py, view);
|
|
354
|
+
}
|
|
355
|
+
var BUG_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="7" width="8" height="12" rx="4"/><path d="M9.5 7a2.5 2.5 0 0 1 5 0M12 11v8M4 13h4M16 13h4M5 7l3.2 2.4M19 7l-3.2 2.4M5 20l3.2-2.6M19 20l-3.2-2.6"/></svg>';
|
|
356
|
+
var STYLE = `
|
|
357
|
+
:host { all: initial; }
|
|
358
|
+
* { box-sizing: border-box; }
|
|
359
|
+
.bug {
|
|
360
|
+
position: fixed; z-index: 2147483000; left: 0; top: 0;
|
|
361
|
+
width: 44px; height: 44px; border-radius: 22px; padding: 0; margin: 0;
|
|
362
|
+
display: var(--fs-feedback-display, grid); place-items: center; cursor: grab;
|
|
363
|
+
background: var(--surface); color: var(--ink); border: 1px solid var(--line);
|
|
364
|
+
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2), 0 2px 5px rgba(0, 0, 0, 0.12);
|
|
365
|
+
-webkit-tap-highlight-color: transparent; touch-action: none; user-select: none; -webkit-user-select: none;
|
|
366
|
+
will-change: transform;
|
|
367
|
+
}
|
|
368
|
+
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
|
|
369
|
+
.bug {
|
|
370
|
+
background: color-mix(in srgb, var(--surface) 74%, transparent);
|
|
371
|
+
-webkit-backdrop-filter: blur(14px) saturate(160%); backdrop-filter: blur(14px) saturate(160%);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
.bug svg { width: 20px; height: 20px; pointer-events: none; }
|
|
375
|
+
.bug.down svg { transform: scale(0.9); }
|
|
376
|
+
.bug.dragging { cursor: grabbing; box-shadow: 0 18px 36px rgba(0, 0, 0, 0.26), 0 3px 8px rgba(0, 0, 0, 0.14); }
|
|
377
|
+
.bug[data-state^="dock"] { width: 26px; height: 54px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.14); opacity: 0.92; }
|
|
378
|
+
.bug[data-state="dock-right"] { border-radius: 16px 0 0 16px; border-right-width: 0; }
|
|
379
|
+
.bug[data-state="dock-left"] { border-radius: 0 16px 16px 0; border-left-width: 0; }
|
|
380
|
+
.bug[data-state^="dock"] svg { width: 16px; height: 16px; }
|
|
381
|
+
.probe {
|
|
382
|
+
position: fixed; visibility: hidden; pointer-events: none; width: 0; height: 0;
|
|
383
|
+
padding: env(safe-area-inset-top, 0px) 0 env(safe-area-inset-bottom, 0px) 0;
|
|
384
|
+
}
|
|
385
|
+
.tuck { align-self: center; font-size: 13px; color: var(--ink-dim); }
|
|
386
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
387
|
+
.bug { background: var(--surface); -webkit-backdrop-filter: none; backdrop-filter: none; }
|
|
388
|
+
}
|
|
389
|
+
@media (prefers-contrast: more) {
|
|
390
|
+
.bug { background: var(--surface); border-color: var(--ink); -webkit-backdrop-filter: none; backdrop-filter: none; }
|
|
391
|
+
}
|
|
392
|
+
.bug:focus-visible, .send:focus-visible, .link:focus-visible, textarea:focus-visible {
|
|
393
|
+
outline: 2px solid var(--ink); outline-offset: 2px;
|
|
394
|
+
}
|
|
395
|
+
.scrim { position: fixed; inset: 0; z-index: 2147483001; background: rgba(0, 0, 0, 0.42); }
|
|
396
|
+
.sheet {
|
|
397
|
+
position: fixed; left: 0; right: 0; bottom: 0; z-index: 2147483002;
|
|
398
|
+
margin: 0 auto; max-width: 480px;
|
|
399
|
+
background: var(--surface); color: var(--ink); border: 1px solid var(--line); border-bottom: 0;
|
|
400
|
+
border-radius: calc(var(--radius) + 6px) calc(var(--radius) + 6px) 0 0;
|
|
401
|
+
padding: 12px 18px calc(20px + env(safe-area-inset-bottom, 0px));
|
|
402
|
+
display: flex; flex-direction: column; gap: 12px;
|
|
403
|
+
font-family: var(--font-body); font-size: 15px; line-height: 1.4;
|
|
404
|
+
}
|
|
405
|
+
.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); }
|
|
409
|
+
.link { background: none; border: 0; padding: 0; font: inherit; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
|
|
410
|
+
textarea {
|
|
411
|
+
width: 100%; min-height: 84px; resize: none; font: inherit; font-size: 16px; color: var(--ink);
|
|
412
|
+
background: var(--fill); border: 1px solid var(--line); border-radius: var(--radius); padding: 13px 15px;
|
|
413
|
+
}
|
|
414
|
+
textarea::placeholder { color: var(--ink-dim); }
|
|
415
|
+
.send {
|
|
416
|
+
font: inherit; font-weight: 500; cursor: pointer; border: 0; padding: 14px;
|
|
417
|
+
background: var(--cta); color: var(--cta-ink); border-radius: var(--button-radius);
|
|
418
|
+
}
|
|
419
|
+
.send[disabled] { opacity: 0.5; cursor: default; }
|
|
420
|
+
.status { margin: 0; font-size: 13px; color: var(--ink-dim); min-height: 1.4em; }
|
|
421
|
+
[hidden] { display: none !important; }
|
|
422
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
423
|
+
.bug {
|
|
424
|
+
transition: width 0.22s cubic-bezier(0.2, 0.8, 0.2, 1), height 0.22s cubic-bezier(0.2, 0.8, 0.2, 1),
|
|
425
|
+
border-radius 0.22s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.22s ease;
|
|
426
|
+
}
|
|
427
|
+
.bug svg { transition: transform 0.1s ease-out, width 0.22s ease, height 0.22s ease; }
|
|
428
|
+
.sheet { animation: rise 0.22s ease-out; }
|
|
429
|
+
@keyframes rise { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
430
|
+
}
|
|
431
|
+
`;
|
|
432
|
+
var mounted = null;
|
|
433
|
+
var feedbackOptedOut = (doc = document) => doc.documentElement.getAttribute("data-fs-feedback") === "off";
|
|
434
|
+
var SCROLL_OPTIONS = { capture: true, passive: true };
|
|
435
|
+
function mountFlowFeedback(options) {
|
|
436
|
+
if (typeof document === "undefined" || !document.body) return null;
|
|
437
|
+
if (mounted) return mounted;
|
|
438
|
+
if (feedbackOptedOut()) return null;
|
|
439
|
+
const host = document.createElement("div");
|
|
440
|
+
host.setAttribute(FEEDBACK_HOST_ATTR, "");
|
|
441
|
+
const root = host.attachShadow({ mode: "open" });
|
|
442
|
+
root.innerHTML = `<style>${STYLE}</style>
|
|
443
|
+
<button type="button" class="bug" data-state="float" aria-label="Send feedback on this page">${BUG_ICON}</button>
|
|
444
|
+
<div class="probe" aria-hidden="true"></div>
|
|
445
|
+
<div class="scrim" hidden></div>
|
|
446
|
+
<div class="sheet" role="dialog" aria-modal="true" aria-labelledby="fsf-title" hidden>
|
|
447
|
+
<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>
|
|
454
|
+
</div>`;
|
|
455
|
+
const q = (selector) => root.querySelector(selector);
|
|
456
|
+
const bug = q(".bug");
|
|
457
|
+
const scrim = q(".scrim");
|
|
458
|
+
const sheet = q(".sheet");
|
|
459
|
+
const shot = q(".shot");
|
|
460
|
+
const img = q(".shot img");
|
|
461
|
+
const remove = q(".remove");
|
|
462
|
+
const text = q("textarea");
|
|
463
|
+
const status = q(".status");
|
|
464
|
+
const send = q(".send");
|
|
465
|
+
const tuck = q(".tuck");
|
|
466
|
+
const probe = q(".probe");
|
|
467
|
+
let theme = readFlowTheme();
|
|
468
|
+
let screenshot = null;
|
|
469
|
+
let busy = false;
|
|
470
|
+
let closeTimer = null;
|
|
471
|
+
let placeTimer = null;
|
|
472
|
+
let autoBottom = PLACEMENT_BASE;
|
|
473
|
+
let spot = { state: "float", x: 0, y: 0 };
|
|
474
|
+
let placedByPerson = false;
|
|
475
|
+
let velocity = { x: 0, y: 0 };
|
|
476
|
+
let frame = 0;
|
|
477
|
+
const px = (name) => {
|
|
478
|
+
const raw = getComputedStyle(host).getPropertyValue(name).trim();
|
|
479
|
+
const n = raw ? parseFloat(raw) : NaN;
|
|
480
|
+
return Number.isFinite(n) ? n : null;
|
|
481
|
+
};
|
|
482
|
+
const viewport = () => {
|
|
483
|
+
const style = getComputedStyle(probe);
|
|
484
|
+
return {
|
|
485
|
+
width: window.innerWidth,
|
|
486
|
+
height: window.innerHeight,
|
|
487
|
+
top: parseFloat(style.paddingTop) || 0,
|
|
488
|
+
bottom: parseFloat(style.paddingBottom) || 0
|
|
489
|
+
};
|
|
490
|
+
};
|
|
491
|
+
const draw = () => {
|
|
492
|
+
bug.style.transform = `translate3d(${spot.x}px, ${spot.y}px, 0)`;
|
|
493
|
+
};
|
|
494
|
+
const setState = (next) => {
|
|
495
|
+
spot.state = next;
|
|
496
|
+
bug.dataset.state = next;
|
|
497
|
+
};
|
|
498
|
+
const remember = (spot2) => {
|
|
499
|
+
if (!placedByPerson) return;
|
|
500
|
+
try {
|
|
501
|
+
const view = viewport();
|
|
502
|
+
const width = spot2.state === "float" ? BUTTON_SIZE : TAB_WIDTH;
|
|
503
|
+
localStorage.setItem(
|
|
504
|
+
SPOT_KEY,
|
|
505
|
+
JSON.stringify({
|
|
506
|
+
state: spot2.state,
|
|
507
|
+
left: spot2.x + width / 2 < view.width / 2,
|
|
508
|
+
y: view.height > 0 ? spot2.y / view.height : 0
|
|
509
|
+
})
|
|
510
|
+
);
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
const still = () => typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
515
|
+
const springTo = (to, flicked) => {
|
|
516
|
+
cancelAnimationFrame(frame);
|
|
517
|
+
frame = 0;
|
|
518
|
+
setState(to.state);
|
|
519
|
+
remember(to);
|
|
520
|
+
if (still() || typeof requestAnimationFrame !== "function") {
|
|
521
|
+
spot = { ...to };
|
|
522
|
+
velocity = { x: 0, y: 0 };
|
|
523
|
+
draw();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const omega = 2 * Math.PI / 0.4;
|
|
527
|
+
const k = omega * omega;
|
|
528
|
+
const c = 2 * (flicked ? 0.8 : 1) * omega;
|
|
529
|
+
let last = performance.now();
|
|
530
|
+
const tick = (now) => {
|
|
531
|
+
const dt = Math.min(0.032, (now - last) / 1e3);
|
|
532
|
+
last = now;
|
|
533
|
+
velocity.x += (-k * (spot.x - to.x) - c * velocity.x) * dt;
|
|
534
|
+
velocity.y += (-k * (spot.y - to.y) - c * velocity.y) * dt;
|
|
535
|
+
spot.x += velocity.x * dt;
|
|
536
|
+
spot.y += velocity.y * dt;
|
|
537
|
+
const close2 = Math.abs(spot.x - to.x) < 0.3 && Math.abs(spot.y - to.y) < 0.3 && Math.hypot(velocity.x, velocity.y) < 6;
|
|
538
|
+
if (close2) {
|
|
539
|
+
spot = { ...to };
|
|
540
|
+
velocity = { x: 0, y: 0 };
|
|
541
|
+
frame = 0;
|
|
542
|
+
draw();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
draw();
|
|
546
|
+
frame = requestAnimationFrame(tick);
|
|
547
|
+
};
|
|
548
|
+
frame = requestAnimationFrame(tick);
|
|
549
|
+
};
|
|
550
|
+
const place = () => {
|
|
551
|
+
placeTimer = null;
|
|
552
|
+
if (!sheet.hidden || bug.hidden || drag || frame) return;
|
|
553
|
+
const view = viewport();
|
|
554
|
+
if (placedByPerson || spot.state !== "float") {
|
|
555
|
+
const width = spot.state === "float" ? BUTTON_SIZE : TAB_WIDTH;
|
|
556
|
+
spot = restingSpot(spot.state, spot.x + width / 2 < view.width / 2, spot.y, view);
|
|
557
|
+
draw();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const right = px("--fs-feedback-right") ?? 14;
|
|
561
|
+
const left = view.width - right - BUTTON_SIZE;
|
|
562
|
+
const pinned = px("--fs-feedback-bottom");
|
|
563
|
+
if (pinned === null && typeof document.elementsFromPoint === "function") {
|
|
564
|
+
autoBottom = choosePlacement(
|
|
565
|
+
{
|
|
566
|
+
left,
|
|
567
|
+
top: view.height - view.bottom - autoBottom - BUTTON_SIZE,
|
|
568
|
+
size: BUTTON_SIZE,
|
|
569
|
+
bottom: autoBottom
|
|
570
|
+
},
|
|
571
|
+
view.height,
|
|
572
|
+
occupancyReader(host)
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
const bottom = pinned ?? autoBottom;
|
|
576
|
+
spot = { state: "float", x: left, y: view.height - view.bottom - bottom - BUTTON_SIZE };
|
|
577
|
+
draw();
|
|
578
|
+
};
|
|
579
|
+
const schedulePlace = () => {
|
|
580
|
+
if (placeTimer) clearTimeout(placeTimer);
|
|
581
|
+
placeTimer = setTimeout(place, 150);
|
|
582
|
+
};
|
|
583
|
+
let drag = null;
|
|
584
|
+
let suppressClick = false;
|
|
585
|
+
const onDown = (event) => {
|
|
586
|
+
if (!sheet.hidden || busy) return;
|
|
587
|
+
bug.setPointerCapture?.(event.pointerId);
|
|
588
|
+
cancelAnimationFrame(frame);
|
|
589
|
+
frame = 0;
|
|
590
|
+
velocity = { x: 0, y: 0 };
|
|
591
|
+
drag = {
|
|
592
|
+
id: event.pointerId,
|
|
593
|
+
ox: event.clientX - spot.x,
|
|
594
|
+
oy: event.clientY - spot.y,
|
|
595
|
+
sx: event.clientX,
|
|
596
|
+
sy: event.clientY,
|
|
597
|
+
moved: false,
|
|
598
|
+
history: [{ t: event.timeStamp, x: event.clientX, y: event.clientY }]
|
|
599
|
+
};
|
|
600
|
+
bug.classList.add("down");
|
|
601
|
+
};
|
|
602
|
+
const onMove = (event) => {
|
|
603
|
+
if (!drag || event.pointerId !== drag.id) return;
|
|
604
|
+
const dx = event.clientX - drag.sx;
|
|
605
|
+
const dy = event.clientY - drag.sy;
|
|
606
|
+
if (!drag.moved && Math.hypot(dx, dy) < 8) return;
|
|
607
|
+
if (!drag.moved) {
|
|
608
|
+
drag.moved = true;
|
|
609
|
+
bug.classList.add("dragging");
|
|
610
|
+
}
|
|
611
|
+
drag.history.push({ t: event.timeStamp, x: event.clientX, y: event.clientY });
|
|
612
|
+
if (drag.history.length > 6) drag.history.shift();
|
|
613
|
+
const view = viewport();
|
|
614
|
+
if (spot.state !== "float") {
|
|
615
|
+
const inward = spot.state === "dock-left" ? dx : -dx;
|
|
616
|
+
if (inward <= 18) {
|
|
617
|
+
spot = restingSpot(spot.state, spot.state === "dock-left", event.clientY - drag.oy, view);
|
|
618
|
+
draw();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
setState("float");
|
|
622
|
+
drag.ox = BUTTON_SIZE / 2;
|
|
623
|
+
drag.oy = BUTTON_SIZE / 2;
|
|
624
|
+
}
|
|
625
|
+
let x = event.clientX - drag.ox;
|
|
626
|
+
let y = event.clientY - drag.oy;
|
|
627
|
+
const maxX = view.width - BUTTON_SIZE;
|
|
628
|
+
const maxY = view.height - BUTTON_SIZE;
|
|
629
|
+
if (x < 0) x = rubberband(x, 60);
|
|
630
|
+
else if (x > maxX) x = maxX + rubberband(x - maxX, 60);
|
|
631
|
+
if (y < 0) y = rubberband(y, 80);
|
|
632
|
+
else if (y > maxY) y = maxY + rubberband(y - maxY, 80);
|
|
633
|
+
spot.x = x;
|
|
634
|
+
spot.y = y;
|
|
635
|
+
draw();
|
|
636
|
+
};
|
|
637
|
+
const onUp = (event) => {
|
|
638
|
+
if (!drag || event.pointerId !== drag.id) return;
|
|
639
|
+
const ended = drag;
|
|
640
|
+
drag = null;
|
|
641
|
+
bug.classList.remove("down", "dragging");
|
|
642
|
+
if (!ended.moved) return;
|
|
643
|
+
suppressClick = true;
|
|
644
|
+
setTimeout(() => {
|
|
645
|
+
suppressClick = false;
|
|
646
|
+
}, 0);
|
|
647
|
+
const first = ended.history[0];
|
|
648
|
+
const last = ended.history[ended.history.length - 1];
|
|
649
|
+
const dt = Math.max(1, last.t - first.t);
|
|
650
|
+
const paused = event.timeStamp - last.t > 90;
|
|
651
|
+
velocity = paused ? { x: 0, y: 0 } : { x: (last.x - first.x) / dt * 1e3, y: (last.y - first.y) / dt * 1e3 };
|
|
652
|
+
placedByPerson = true;
|
|
653
|
+
springTo(settleRelease(spot, velocity, viewport()), Math.hypot(velocity.x, velocity.y) > 350);
|
|
654
|
+
};
|
|
655
|
+
const observer = typeof MutationObserver === "function" ? new MutationObserver((records) => {
|
|
656
|
+
if (records.some((r) => r.target !== host)) schedulePlace();
|
|
657
|
+
}) : null;
|
|
658
|
+
const paint = () => {
|
|
659
|
+
theme = readFlowTheme();
|
|
660
|
+
const vars = {
|
|
661
|
+
"--surface": theme.surface,
|
|
662
|
+
"--ink": theme.ink,
|
|
663
|
+
"--ink-dim": theme.inkDim,
|
|
664
|
+
"--line": theme.line,
|
|
665
|
+
"--fill": theme.fill,
|
|
666
|
+
"--cta": theme.cta,
|
|
667
|
+
"--cta-ink": theme.ctaInk,
|
|
668
|
+
"--font-body": theme.fontBody,
|
|
669
|
+
"--font-display": theme.fontDisplay,
|
|
670
|
+
"--display-weight": theme.displayWeight,
|
|
671
|
+
"--radius": theme.radius,
|
|
672
|
+
"--button-radius": theme.buttonRadius
|
|
673
|
+
};
|
|
674
|
+
for (const [name, value] of Object.entries(vars)) host.style.setProperty(name, value);
|
|
675
|
+
host.style.colorScheme = theme.dark ? "dark" : "light";
|
|
676
|
+
};
|
|
677
|
+
const sync = () => {
|
|
678
|
+
send.disabled = busy || text.value.trim().length === 0;
|
|
679
|
+
};
|
|
680
|
+
const setShot = (url) => {
|
|
681
|
+
screenshot = url;
|
|
682
|
+
shot.hidden = url === null;
|
|
683
|
+
if (url) img.src = url;
|
|
684
|
+
else img.removeAttribute("src");
|
|
685
|
+
};
|
|
686
|
+
const close = () => {
|
|
687
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
688
|
+
closeTimer = null;
|
|
689
|
+
sheet.hidden = true;
|
|
690
|
+
scrim.hidden = true;
|
|
691
|
+
bug.hidden = false;
|
|
692
|
+
setShot(null);
|
|
693
|
+
text.value = "";
|
|
694
|
+
status.textContent = "";
|
|
695
|
+
busy = false;
|
|
696
|
+
sync();
|
|
697
|
+
schedulePlace();
|
|
698
|
+
};
|
|
699
|
+
const open = async () => {
|
|
700
|
+
if (!sheet.hidden || busy) return;
|
|
701
|
+
busy = true;
|
|
702
|
+
paint();
|
|
703
|
+
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);
|
|
708
|
+
tuck.hidden = spot.state !== "float";
|
|
709
|
+
scrim.hidden = false;
|
|
710
|
+
sheet.hidden = false;
|
|
711
|
+
sync();
|
|
712
|
+
text.focus();
|
|
713
|
+
};
|
|
714
|
+
const submit = async () => {
|
|
715
|
+
const message = text.value.trim();
|
|
716
|
+
if (!message || busy) return;
|
|
717
|
+
busy = true;
|
|
718
|
+
sync();
|
|
719
|
+
status.textContent = "";
|
|
720
|
+
try {
|
|
721
|
+
await options.submit({
|
|
722
|
+
message,
|
|
723
|
+
...screenshot ? { screenshot } : {},
|
|
724
|
+
...document.body.dataset.pageKey ? { pageKey: document.body.dataset.pageKey } : {},
|
|
725
|
+
pagePath: window.location.pathname.slice(-200),
|
|
726
|
+
viewport: { width: window.innerWidth, height: window.innerHeight }
|
|
727
|
+
});
|
|
728
|
+
status.textContent = "Sent.";
|
|
729
|
+
closeTimer = setTimeout(() => {
|
|
730
|
+
close();
|
|
731
|
+
bug.focus();
|
|
732
|
+
}, 1200);
|
|
733
|
+
} catch {
|
|
734
|
+
busy = false;
|
|
735
|
+
status.textContent = "That did not send. Try again.";
|
|
736
|
+
sync();
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
bug.addEventListener("click", () => {
|
|
740
|
+
if (!suppressClick) void open();
|
|
741
|
+
});
|
|
742
|
+
bug.addEventListener("pointerdown", onDown);
|
|
743
|
+
bug.addEventListener("pointermove", onMove);
|
|
744
|
+
bug.addEventListener("pointerup", onUp);
|
|
745
|
+
bug.addEventListener("pointercancel", onUp);
|
|
746
|
+
tuck.addEventListener("click", () => {
|
|
747
|
+
const view = viewport();
|
|
748
|
+
const left = spot.x + BUTTON_SIZE / 2 < view.width / 2;
|
|
749
|
+
close();
|
|
750
|
+
placedByPerson = true;
|
|
751
|
+
springTo(
|
|
752
|
+
restingSpot(
|
|
753
|
+
left ? "dock-left" : "dock-right",
|
|
754
|
+
left,
|
|
755
|
+
spot.y + (BUTTON_SIZE - TAB_HEIGHT) / 2,
|
|
756
|
+
view
|
|
757
|
+
),
|
|
758
|
+
false
|
|
759
|
+
);
|
|
760
|
+
});
|
|
761
|
+
scrim.addEventListener("click", close);
|
|
762
|
+
remove.addEventListener("click", () => setShot(null));
|
|
763
|
+
text.addEventListener("input", sync);
|
|
764
|
+
send.addEventListener("click", () => void submit());
|
|
765
|
+
sheet.addEventListener("keydown", (event) => {
|
|
766
|
+
if (event.key === "Escape") {
|
|
767
|
+
close();
|
|
768
|
+
bug.focus();
|
|
769
|
+
}
|
|
770
|
+
event.stopPropagation();
|
|
771
|
+
});
|
|
772
|
+
document.body.appendChild(host);
|
|
773
|
+
paint();
|
|
774
|
+
try {
|
|
775
|
+
const saved = JSON.parse(localStorage.getItem(SPOT_KEY) ?? "null");
|
|
776
|
+
if (saved && typeof saved.y === "number" && (saved.state === "float" || saved.state === "dock-left" || saved.state === "dock-right")) {
|
|
777
|
+
const view = viewport();
|
|
778
|
+
placedByPerson = true;
|
|
779
|
+
setState(saved.state);
|
|
780
|
+
spot = restingSpot(saved.state, saved.left === true, saved.y * view.height, view);
|
|
781
|
+
}
|
|
782
|
+
} catch {
|
|
783
|
+
}
|
|
784
|
+
place();
|
|
785
|
+
schedulePlace();
|
|
786
|
+
observer?.observe(document.body, {
|
|
787
|
+
childList: true,
|
|
788
|
+
subtree: true,
|
|
789
|
+
attributes: true,
|
|
790
|
+
attributeFilter: ["class", "style", "hidden"]
|
|
791
|
+
});
|
|
792
|
+
window.addEventListener("resize", schedulePlace);
|
|
793
|
+
window.addEventListener("load", schedulePlace);
|
|
794
|
+
window.addEventListener("scroll", schedulePlace, SCROLL_OPTIONS);
|
|
795
|
+
document.addEventListener("transitionend", schedulePlace, SCROLL_OPTIONS);
|
|
796
|
+
document.addEventListener("animationend", schedulePlace, SCROLL_OPTIONS);
|
|
797
|
+
options.onReady?.();
|
|
798
|
+
mounted = {
|
|
799
|
+
host,
|
|
800
|
+
refresh: () => {
|
|
801
|
+
if (sheet.hidden) paint();
|
|
802
|
+
},
|
|
803
|
+
open,
|
|
804
|
+
close,
|
|
805
|
+
unmount: () => {
|
|
806
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
807
|
+
if (placeTimer) clearTimeout(placeTimer);
|
|
808
|
+
cancelAnimationFrame(frame);
|
|
809
|
+
observer?.disconnect();
|
|
810
|
+
window.removeEventListener("resize", schedulePlace);
|
|
811
|
+
window.removeEventListener("load", schedulePlace);
|
|
812
|
+
window.removeEventListener("scroll", schedulePlace, SCROLL_OPTIONS);
|
|
813
|
+
document.removeEventListener("transitionend", schedulePlace, SCROLL_OPTIONS);
|
|
814
|
+
document.removeEventListener("animationend", schedulePlace, SCROLL_OPTIONS);
|
|
815
|
+
host.remove();
|
|
816
|
+
mounted = null;
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
return mounted;
|
|
820
|
+
}
|
|
821
|
+
var unmountFlowFeedback = () => mounted?.unmount();
|
|
822
|
+
|
|
42
823
|
// src/session.ts
|
|
43
824
|
function readSessionLaunchParams() {
|
|
44
825
|
if (typeof window === "undefined") return null;
|
|
@@ -127,10 +908,12 @@ var HttpSessionClient = class {
|
|
|
127
908
|
__publicField(this, "apiBase");
|
|
128
909
|
__publicField(this, "tokenPromise");
|
|
129
910
|
__publicField(this, "opener");
|
|
911
|
+
__publicField(this, "hostOrigin");
|
|
130
912
|
this.slug = params.slug;
|
|
131
913
|
this.webBase = params.webBase;
|
|
132
914
|
this.mobileBase = params.mobileBase ?? null;
|
|
133
915
|
this.apiBase = params.apiBase;
|
|
916
|
+
this.hostOrigin = hostOriginOf(params);
|
|
134
917
|
this.opener = opener ?? parentOpener(hostOriginOf(params));
|
|
135
918
|
this.tokenPromise = this.acquireTicket(params).then((t) => this.redeem(t));
|
|
136
919
|
}
|
|
@@ -164,13 +947,32 @@ var HttpSessionClient = class {
|
|
|
164
947
|
}
|
|
165
948
|
const json = await res.json();
|
|
166
949
|
const data = json.data;
|
|
167
|
-
const
|
|
950
|
+
const wire = data.state ?? data;
|
|
951
|
+
const view = toStateView(wire);
|
|
168
952
|
this.latestClaims = view.viewer?.claims ?? null;
|
|
953
|
+
this.syncFeedback(wire.feedbackEnabled === true);
|
|
169
954
|
return view;
|
|
170
955
|
}
|
|
171
956
|
get claims() {
|
|
172
957
|
return this.latestClaims;
|
|
173
958
|
}
|
|
959
|
+
/** Flow feedback: every state read says whether the bug icon is on for this ring. On, the
|
|
960
|
+
* icon mounts once and re-reads the page's look on each read; off, it leaves. */
|
|
961
|
+
syncFeedback(enabled) {
|
|
962
|
+
if (typeof document === "undefined") return;
|
|
963
|
+
if (!enabled) {
|
|
964
|
+
unmountFlowFeedback();
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const handle = mountFlowFeedback({
|
|
968
|
+
submit: (report) => this.request("feedback", report),
|
|
969
|
+
onReady: () => {
|
|
970
|
+
if (window.parent !== window)
|
|
971
|
+
window.parent.postMessage({ type: "flowstate:feedback-ready" }, this.hostOrigin);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
handle?.refresh();
|
|
975
|
+
}
|
|
174
976
|
join() {
|
|
175
977
|
return this.call("/enter", { method: "POST" });
|
|
176
978
|
}
|
|
@@ -925,12 +1727,16 @@ export {
|
|
|
925
1727
|
applyWidgetTheme,
|
|
926
1728
|
createMockSession,
|
|
927
1729
|
createServiceClient,
|
|
1730
|
+
feedbackOptedOut,
|
|
928
1731
|
getInitialTheme,
|
|
929
1732
|
hostOriginOf,
|
|
930
1733
|
initSession,
|
|
931
1734
|
initWidgetTheme,
|
|
1735
|
+
mountFlowFeedback,
|
|
932
1736
|
onArtifactIngested,
|
|
933
1737
|
onThemeChange,
|
|
934
|
-
|
|
1738
|
+
readFlowTheme,
|
|
1739
|
+
readSessionLaunchParams,
|
|
1740
|
+
unmountFlowFeedback
|
|
935
1741
|
};
|
|
936
1742
|
//# sourceMappingURL=index.mjs.map
|