@mahmulp/feedback-sdk 0.1.0 → 0.2.0
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/LICENSE +21 -21
- package/README.md +111 -110
- package/dist/index.cjs +133 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.global.js +14 -28
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +133 -39
- package/dist/index.js.map +1 -1
- package/dist/mock.cjs.map +1 -1
- package/dist/mock.js.map +1 -1
- package/dist/svelte.js +133 -39
- package/dist/svelte.js.map +1 -1
- package/package.json +5 -7
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Machmul Pratama
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Machmul Pratama
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,110 +1,111 @@
|
|
|
1
|
-
# @mahmulp/feedback-sdk
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@mahmulp/feedback-sdk)
|
|
4
|
-
[](https://www.npmjs.com/package/@mahmulp/feedback-sdk)
|
|
4
|
+
[](https://www.npmjs.com/package/@mahmulp/feedback-sdk)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
Framework-agnostic visual feedback SDK for prototype review. Reviewers pin comments to UI elements; you get back a stable DOM selector, percentage + pixel coordinates, viewport metadata, and an optional screenshot.
|
|
8
|
+
|
|
9
|
+
Designed for **self-hosted** setups: pair this SDK with a backend (we ship one in the same repo) and own your data.
|
|
10
|
+
|
|
11
|
+
- Floating launcher widget built in (toggle feedback mode, hide pins, hide launcher)
|
|
12
|
+
- Stable DOM selectors that survive layout changes
|
|
13
|
+
- Drag-to-move pins with optimistic update
|
|
14
|
+
- Screenshot capture via dynamically loaded `html2canvas-pro` (bundled as a direct dependency)
|
|
15
|
+
- Svelte adapter for ergonomic SvelteKit integration
|
|
16
|
+
- Works in React, Vue, and plain HTML via the same core API
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @mahmulp/feedback-sdk
|
|
22
|
+
bun add @mahmulp/feedback-sdk
|
|
23
|
+
pnpm add @mahmulp/feedback-sdk
|
|
24
|
+
yarn add @mahmulp/feedback-sdk
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
You always need an `apiUrl` and an `apiKey`. The key tells the API which project this prototype belongs to — generate one in your dashboard's project settings.
|
|
30
|
+
|
|
31
|
+
### Vanilla / React / Vue
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { initFeedback } from '@mahmulp/feedback-sdk'
|
|
35
|
+
|
|
36
|
+
initFeedback({
|
|
37
|
+
apiUrl: 'https://feedback.example.com',
|
|
38
|
+
apiKey: 'mp_…',
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
That's it. A floating launcher appears in the bottom-right corner.
|
|
43
|
+
|
|
44
|
+
### Svelte / SvelteKit
|
|
45
|
+
|
|
46
|
+
```svelte
|
|
47
|
+
<script lang="ts">
|
|
48
|
+
import { feedback } from '@mahmulp/feedback-sdk/svelte'
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<div use:feedback={{
|
|
52
|
+
apiUrl: 'https://feedback.example.com',
|
|
53
|
+
apiKey: 'mp_…',
|
|
54
|
+
}}>
|
|
55
|
+
<slot />
|
|
56
|
+
</div>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Local development without a backend
|
|
60
|
+
|
|
61
|
+
For demos, e2e tests, or styling work — use the in-memory mock transport:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { initFeedback } from '@mahmulp/feedback-sdk'
|
|
65
|
+
import { createMockTransport } from '@mahmulp/feedback-sdk/mock'
|
|
66
|
+
|
|
67
|
+
initFeedback({ transport: createMockTransport() })
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## What it does
|
|
71
|
+
|
|
72
|
+
When the user enters feedback mode (via the launcher or `setEnabled(true)`):
|
|
73
|
+
|
|
74
|
+
- **Hover** any element → outline highlight + tag/class HUD.
|
|
75
|
+
- **Click** → composer popover (name, email, comment, optional screenshot).
|
|
76
|
+
- **Alt-click** → walks one parent up so you can pin a coarser element.
|
|
77
|
+
- **Esc** → cancel.
|
|
78
|
+
|
|
79
|
+
Existing pins are rendered as draggable markers. Click a pin to open its thread, reply, or change status. Drop a `data-feedback-id="some-key"` attribute on important elements to make their selectors human-readable and refactor-proof.
|
|
80
|
+
|
|
81
|
+
## Public API
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import {
|
|
85
|
+
initFeedback,
|
|
86
|
+
setFeedbackEnabled,
|
|
87
|
+
destroyFeedback,
|
|
88
|
+
resolveSelector,
|
|
89
|
+
findElement,
|
|
90
|
+
createHttpTransport,
|
|
91
|
+
captureViewport,
|
|
92
|
+
} from '@mahmulp/feedback-sdk'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { feedback, feedbackEnabled } from '@mahmulp/feedback-sdk/svelte'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { createMockTransport } from '@mahmulp/feedback-sdk/mock'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The full set of options is documented in `InitFeedbackOptions` (TypeScript types ship with the package).
|
|
104
|
+
|
|
105
|
+
## Self-host the backend
|
|
106
|
+
|
|
107
|
+
The SDK pairs with a small self-hostable backend (Hono on Bun + PostgreSQL or in-memory store) that lives in the same monorepo: <https://github.com/MahmulP/feedback-prototype>. The dashboard there manages users, projects, and per-project API keys.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT © Mahmul Pratama
|
package/dist/index.cjs
CHANGED
|
@@ -184,7 +184,10 @@ var LauncherManager = class {
|
|
|
184
184
|
__publicField(this, "state", {
|
|
185
185
|
enabled: false,
|
|
186
186
|
pinsVisible: true,
|
|
187
|
-
|
|
187
|
+
// Start collapsed — only the small reveal bubble is visible until the
|
|
188
|
+
// user clicks it. Less visual weight on the prototype out of the box;
|
|
189
|
+
// user opts in by clicking the bubble icon in the bottom-right.
|
|
190
|
+
launcherVisible: false
|
|
188
191
|
});
|
|
189
192
|
this.launcherEl = document.createElement("div");
|
|
190
193
|
this.launcherEl.className = "launcher";
|
|
@@ -236,6 +239,7 @@ var LauncherManager = class {
|
|
|
236
239
|
/** True when an event originated from the launcher's own UI. */
|
|
237
240
|
ownsNode(node) {
|
|
238
241
|
if (!node) return false;
|
|
242
|
+
if (typeof Node === "undefined" || !(node instanceof Node)) return false;
|
|
239
243
|
return this.launcherEl.contains(node) || this.revealEl.contains(node);
|
|
240
244
|
}
|
|
241
245
|
destroy() {
|
|
@@ -317,11 +321,34 @@ var PopoverManager = class {
|
|
|
317
321
|
});
|
|
318
322
|
return el;
|
|
319
323
|
}
|
|
324
|
+
/** True when a thread for this feedback is already open. */
|
|
325
|
+
isThreadOpenFor(feedbackId) {
|
|
326
|
+
return this.current?.type === "thread" && this.current.feedbackId === feedbackId;
|
|
327
|
+
}
|
|
320
328
|
showThread(feedback, anchor, cb) {
|
|
329
|
+
if (this.current?.type === "thread" && this.current.feedbackId === feedback.id) {
|
|
330
|
+
const fresh = this.buildThread(feedback, cb);
|
|
331
|
+
this.current.el.replaceWith(fresh);
|
|
332
|
+
this.current = {
|
|
333
|
+
type: "thread",
|
|
334
|
+
el: fresh,
|
|
335
|
+
pageX: anchor.pageX,
|
|
336
|
+
pageY: anchor.pageY,
|
|
337
|
+
feedbackId: feedback.id
|
|
338
|
+
};
|
|
339
|
+
this.repositionInternal();
|
|
340
|
+
return fresh;
|
|
341
|
+
}
|
|
321
342
|
this.hide();
|
|
322
343
|
const el = this.buildThread(feedback, cb);
|
|
323
344
|
this.layer.appendChild(el);
|
|
324
|
-
this.current = {
|
|
345
|
+
this.current = {
|
|
346
|
+
type: "thread",
|
|
347
|
+
el,
|
|
348
|
+
pageX: anchor.pageX,
|
|
349
|
+
pageY: anchor.pageY,
|
|
350
|
+
feedbackId: feedback.id
|
|
351
|
+
};
|
|
325
352
|
this.repositionInternal();
|
|
326
353
|
return el;
|
|
327
354
|
}
|
|
@@ -1029,33 +1056,65 @@ var Overlay = class {
|
|
|
1029
1056
|
});
|
|
1030
1057
|
}
|
|
1031
1058
|
layoutPins() {
|
|
1032
|
-
const
|
|
1059
|
+
const existing = /* @__PURE__ */ new Map();
|
|
1060
|
+
for (const node of Array.from(this.pinLayer.children)) {
|
|
1061
|
+
const id = node.dataset.feedbackId;
|
|
1062
|
+
if (id) existing.set(id, node);
|
|
1063
|
+
}
|
|
1064
|
+
const desiredOrder = [];
|
|
1065
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1033
1066
|
for (const fb of this.pendingFeedback) {
|
|
1067
|
+
seen.add(fb.id);
|
|
1034
1068
|
const target = findElement(fb.selector);
|
|
1035
1069
|
const projected = projectCoordinates(target, fb.coordinates);
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
if (
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1070
|
+
const left = `${projected.x - window.scrollX}px`;
|
|
1071
|
+
const top = `${projected.y - window.scrollY}px`;
|
|
1072
|
+
let pin = existing.get(fb.id);
|
|
1073
|
+
if (pin) {
|
|
1074
|
+
const classes = ["pin"];
|
|
1075
|
+
if (projected.orphaned) classes.push("orphaned");
|
|
1076
|
+
if (fb.status === "resolved") classes.push("resolved");
|
|
1077
|
+
if (fb.status === "archived") classes.push("archived");
|
|
1078
|
+
if (pin.classList.contains("dragging")) classes.push("dragging");
|
|
1079
|
+
pin.className = classes.join(" ");
|
|
1080
|
+
if (pin.style.left !== left) pin.style.left = left;
|
|
1081
|
+
if (pin.style.top !== top) pin.style.top = top;
|
|
1082
|
+
const label = pin.firstElementChild;
|
|
1083
|
+
const nextLabel = String(fb.thread.length || 1);
|
|
1084
|
+
if (label && label.textContent !== nextLabel) label.textContent = nextLabel;
|
|
1085
|
+
} else {
|
|
1086
|
+
pin = document.createElement("button");
|
|
1087
|
+
pin.type = "button";
|
|
1088
|
+
const classes = ["pin"];
|
|
1089
|
+
if (projected.orphaned) classes.push("orphaned");
|
|
1090
|
+
if (fb.status === "resolved") classes.push("resolved");
|
|
1091
|
+
if (fb.status === "archived") classes.push("archived");
|
|
1092
|
+
pin.className = classes.join(" ");
|
|
1093
|
+
pin.dataset.feedbackId = fb.id;
|
|
1094
|
+
pin.setAttribute("aria-label", `Feedback ${fb.id}`);
|
|
1095
|
+
pin.style.left = left;
|
|
1096
|
+
pin.style.top = top;
|
|
1097
|
+
const label = document.createElement("span");
|
|
1098
|
+
label.textContent = String(fb.thread.length || 1);
|
|
1099
|
+
pin.appendChild(label);
|
|
1100
|
+
pin.addEventListener("click", (e) => {
|
|
1101
|
+
e.stopPropagation();
|
|
1102
|
+
if (this.draggingPin === fb.id) return;
|
|
1103
|
+
const fresh = this.pendingFeedback.find((f) => f.id === fb.id) ?? fb;
|
|
1104
|
+
this.onPinClick?.(fresh);
|
|
1105
|
+
});
|
|
1106
|
+
this.attachDragHandlers(pin, fb);
|
|
1107
|
+
}
|
|
1108
|
+
desiredOrder.push(pin);
|
|
1109
|
+
}
|
|
1110
|
+
for (const [id, node] of existing) {
|
|
1111
|
+
if (!seen.has(id)) node.remove();
|
|
1112
|
+
}
|
|
1113
|
+
for (let i = 0; i < desiredOrder.length; i++) {
|
|
1114
|
+
const want = desiredOrder[i];
|
|
1115
|
+
const have = this.pinLayer.children[i];
|
|
1116
|
+
if (have !== want) this.pinLayer.insertBefore(want, have ?? null);
|
|
1057
1117
|
}
|
|
1058
|
-
this.pinLayer.replaceChildren(next);
|
|
1059
1118
|
}
|
|
1060
1119
|
destroy() {
|
|
1061
1120
|
if (this.rafHandle !== null) {
|
|
@@ -1174,8 +1233,6 @@ async function captureViewport(options = {}) {
|
|
|
1174
1233
|
const html2canvas = await loadHtml2Canvas();
|
|
1175
1234
|
if (!html2canvas) return null;
|
|
1176
1235
|
const host = document.querySelector(`[${HOST_ATTR2}]`);
|
|
1177
|
-
const previousVisibility = host?.style.visibility ?? "";
|
|
1178
|
-
if (host) host.style.visibility = "hidden";
|
|
1179
1236
|
try {
|
|
1180
1237
|
const canvas = await html2canvas(document.documentElement, {
|
|
1181
1238
|
backgroundColor: null,
|
|
@@ -1188,23 +1245,37 @@ async function captureViewport(options = {}) {
|
|
|
1188
1245
|
windowHeight: window.innerHeight,
|
|
1189
1246
|
logging: false,
|
|
1190
1247
|
useCORS: true,
|
|
1191
|
-
allowTaint: false
|
|
1248
|
+
allowTaint: false,
|
|
1249
|
+
ignoreElements: (el) => {
|
|
1250
|
+
if (host && (el === host || host.contains(el))) return true;
|
|
1251
|
+
if (el instanceof HTMLElement && el.hasAttribute(HOST_ATTR2)) return true;
|
|
1252
|
+
return false;
|
|
1253
|
+
}
|
|
1192
1254
|
});
|
|
1193
1255
|
return await canvasToBlob(canvas, opts.mimeType, opts.quality);
|
|
1194
|
-
} catch {
|
|
1256
|
+
} catch (err) {
|
|
1257
|
+
if (typeof console !== "undefined") {
|
|
1258
|
+
console.warn("[feedback-sdk] screenshot capture failed:", err);
|
|
1259
|
+
}
|
|
1195
1260
|
return null;
|
|
1196
|
-
} finally {
|
|
1197
|
-
if (host) host.style.visibility = previousVisibility;
|
|
1198
1261
|
}
|
|
1199
1262
|
}
|
|
1200
1263
|
var html2canvasPromise = null;
|
|
1264
|
+
var html2canvasMissingWarned = false;
|
|
1201
1265
|
async function loadHtml2Canvas() {
|
|
1202
1266
|
if (html2canvasPromise) return html2canvasPromise;
|
|
1203
1267
|
html2canvasPromise = (async () => {
|
|
1204
1268
|
try {
|
|
1205
|
-
const mod = await import('html2canvas');
|
|
1269
|
+
const mod = await import('html2canvas-pro');
|
|
1206
1270
|
return mod.default ?? mod ?? null;
|
|
1207
|
-
} catch {
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
if (!html2canvasMissingWarned && typeof console !== "undefined") {
|
|
1273
|
+
html2canvasMissingWarned = true;
|
|
1274
|
+
console.warn(
|
|
1275
|
+
"[feedback-sdk] screenshot capture disabled: failed to load `html2canvas-pro`. This shouldn't normally happen \u2014 html2canvas-pro is a direct dependency of the SDK. Pass `captureScreenshots: false` to silence this warning if intended.",
|
|
1276
|
+
err
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1208
1279
|
return null;
|
|
1209
1280
|
}
|
|
1210
1281
|
})();
|
|
@@ -1497,8 +1568,6 @@ function initFeedback(options) {
|
|
|
1497
1568
|
const updated = await transport.reply(fb.id, { author, body });
|
|
1498
1569
|
setAuthor(author);
|
|
1499
1570
|
replaceFeedback(updated);
|
|
1500
|
-
overlay.popoverManager().hide();
|
|
1501
|
-
activeThreadId = null;
|
|
1502
1571
|
openThread(updated);
|
|
1503
1572
|
} catch (err) {
|
|
1504
1573
|
reportError(err);
|
|
@@ -1508,8 +1577,6 @@ function initFeedback(options) {
|
|
|
1508
1577
|
try {
|
|
1509
1578
|
const updated = await transport.setStatus(fb.id, status);
|
|
1510
1579
|
replaceFeedback(updated);
|
|
1511
|
-
overlay.popoverManager().hide();
|
|
1512
|
-
activeThreadId = null;
|
|
1513
1580
|
openThread(updated);
|
|
1514
1581
|
} catch (err) {
|
|
1515
1582
|
reportError(err);
|
|
@@ -1557,7 +1624,8 @@ function initFeedback(options) {
|
|
|
1557
1624
|
}
|
|
1558
1625
|
async function refresh() {
|
|
1559
1626
|
try {
|
|
1560
|
-
const
|
|
1627
|
+
const pageUrl = getPageUrl();
|
|
1628
|
+
const result = await transport.list({ projectId: "", pageUrl });
|
|
1561
1629
|
state.feedbacks = result.items;
|
|
1562
1630
|
overlay.renderPins(state.feedbacks, openThread, onPinDragEnd);
|
|
1563
1631
|
} catch (err) {
|
|
@@ -1587,12 +1655,35 @@ function initFeedback(options) {
|
|
|
1587
1655
|
document.addEventListener("keydown", onKeyDown, true);
|
|
1588
1656
|
window.addEventListener("scroll", onWindowReposition, true);
|
|
1589
1657
|
window.addEventListener("resize", onWindowReposition);
|
|
1658
|
+
let lastSeenUrl = getPageUrl();
|
|
1659
|
+
function onMaybeNavigate() {
|
|
1660
|
+
const next = getPageUrl();
|
|
1661
|
+
if (next === lastSeenUrl) return;
|
|
1662
|
+
lastSeenUrl = next;
|
|
1663
|
+
void refresh();
|
|
1664
|
+
}
|
|
1665
|
+
const originalPushState = history.pushState.bind(history);
|
|
1666
|
+
const originalReplaceState = history.replaceState.bind(history);
|
|
1667
|
+
history.pushState = function patchedPushState(...args) {
|
|
1668
|
+
const result = originalPushState(...args);
|
|
1669
|
+
queueMicrotask(onMaybeNavigate);
|
|
1670
|
+
return result;
|
|
1671
|
+
};
|
|
1672
|
+
history.replaceState = function patchedReplaceState(...args) {
|
|
1673
|
+
const result = originalReplaceState(...args);
|
|
1674
|
+
queueMicrotask(onMaybeNavigate);
|
|
1675
|
+
return result;
|
|
1676
|
+
};
|
|
1677
|
+
window.addEventListener("popstate", onMaybeNavigate);
|
|
1590
1678
|
void refresh();
|
|
1591
1679
|
overlay.setEnabledStyles(state.enabled);
|
|
1592
1680
|
const wantsLauncher = options.showLauncher !== false;
|
|
1593
1681
|
const launcherState = {
|
|
1594
1682
|
pinsVisible: options.pinsVisible !== false,
|
|
1595
|
-
|
|
1683
|
+
// Default to a collapsed launcher. The user reveals it by clicking the
|
|
1684
|
+
// small bubble button in the bottom-right corner. This keeps the SDK's
|
|
1685
|
+
// visual footprint near zero on first paint of the prototype.
|
|
1686
|
+
launcherVisible: false
|
|
1596
1687
|
};
|
|
1597
1688
|
overlay.setPinsVisible(launcherState.pinsVisible);
|
|
1598
1689
|
function syncLauncher() {
|
|
@@ -1649,6 +1740,9 @@ function initFeedback(options) {
|
|
|
1649
1740
|
document.removeEventListener("keydown", onKeyDown, true);
|
|
1650
1741
|
window.removeEventListener("scroll", onWindowReposition, true);
|
|
1651
1742
|
window.removeEventListener("resize", onWindowReposition);
|
|
1743
|
+
window.removeEventListener("popstate", onMaybeNavigate);
|
|
1744
|
+
history.pushState = originalPushState;
|
|
1745
|
+
history.replaceState = originalReplaceState;
|
|
1652
1746
|
overlay.destroy();
|
|
1653
1747
|
}
|
|
1654
1748
|
};
|